From 52f441bf5d57bc42e9782408cfc044aae37fe49e Mon Sep 17 00:00:00 2001 From: Duncan Uszkay Date: Thu, 18 Jul 2024 15:53:59 -0400 Subject: [PATCH] tentative refactor commit --- packages/app/src/cli/commands/app/logs.ts | 4 +- .../app-logs/dev/poll-app-logs.test.ts | 11 +- .../services/app-logs/dev/poll-app-logs.ts | 69 ++++++----- .../logs-command/render-json-logs.test.ts | 79 +++++++++++++ .../app-logs/logs-command/render-json-logs.ts | 59 ++++++++++ .../components/hooks/usePollAppLogs.test.tsx | 4 +- .../ui/components/hooks/usePollAppLogs.ts | 36 +++--- .../app/src/cli/services/app-logs/utils.ts | 33 ++++++ packages/app/src/cli/services/context.test.ts | 26 +++++ packages/app/src/cli/services/context.ts | 3 +- .../dev/processes/app-logs-polling.ts | 7 +- packages/app/src/cli/services/logs.test.ts | 83 +++++++++++++ packages/app/src/cli/services/logs.ts | 110 ++++-------------- packages/cli/README.md | 82 +++++++------ packages/cli/oclif.manifest.json | 27 ++++- 15 files changed, 443 insertions(+), 190 deletions(-) create mode 100644 packages/app/src/cli/services/app-logs/logs-command/render-json-logs.test.ts create mode 100644 packages/app/src/cli/services/app-logs/logs-command/render-json-logs.ts create mode 100644 packages/app/src/cli/services/logs.test.ts diff --git a/packages/app/src/cli/commands/app/logs.ts b/packages/app/src/cli/commands/app/logs.ts index e7afd4bc986..c439f2a41f5 100644 --- a/packages/app/src/cli/commands/app/logs.ts +++ b/packages/app/src/cli/commands/app/logs.ts @@ -1,7 +1,7 @@ import Dev from './dev.js' import Command from '../../utilities/app-command.js' import {checkFolderIsValidApp} from '../../models/app/loader.js' -import {logs} from '../../services/logs.js' +import {logs, Format} from '../../services/logs.js' import {appLogPollingEnabled} from '../../services/app-logs/utils.js' import {Flags} from '@oclif/core' import {AbortError} from '@shopify/cli-kit/node/error' @@ -58,7 +58,7 @@ export default class Logs extends Command { status: flags.status, configName: flags.config, reset: flags.reset, - json: flags.json, + format: (flags.json ? 'json' : 'text') as Format, } await logs(logOptions) diff --git a/packages/app/src/cli/services/app-logs/dev/poll-app-logs.test.ts b/packages/app/src/cli/services/app-logs/dev/poll-app-logs.test.ts index 60ba2d62a61..2f9c9a631b8 100644 --- a/packages/app/src/cli/services/app-logs/dev/poll-app-logs.test.ts +++ b/packages/app/src/cli/services/app-logs/dev/poll-app-logs.test.ts @@ -339,7 +339,7 @@ describe('pollAppLogs', () => { test('calls resubscribe callback if a 401 is received', async () => { // Given - const response = new Response('errorMessage', {status: 401}) + const response = new Response(JSON.stringify({errors: ['Unauthorized']}), {status: 401}) const mockedFetch = vi.fn().mockResolvedValueOnce(response) vi.mocked(fetch).mockImplementation(mockedFetch) @@ -357,7 +357,9 @@ describe('pollAppLogs', () => { test('displays throttle message, waits, and retries if status is 429', async () => { // Given const outputWarnSpy = vi.spyOn(output, 'outputWarn') - const mockedFetch = vi.fn().mockResolvedValueOnce(new Response('error for 429', {status: 429})) + const mockedFetch = vi + .fn() + .mockResolvedValueOnce(new Response(JSON.stringify({errors: ['error for 429']}), {status: 429})) vi.mocked(fetch).mockImplementation(mockedFetch) // When/Then @@ -369,7 +371,6 @@ describe('pollAppLogs', () => { }) expect(outputWarnSpy).toHaveBeenCalledWith('Request throttled while polling app logs.') - expect(outputWarnSpy).toHaveBeenCalledWith('Retrying in 60 seconds.') expect(vi.getTimerCount()).toEqual(1) }) @@ -379,7 +380,7 @@ describe('pollAppLogs', () => { const outputWarnSpy = vi.spyOn(output, 'outputWarn') // An unexpected error response - const response = new Response('errorMessage', {status: 422}) + const response = new Response(JSON.stringify({errors: ['errorMessage']}), {status: 500}) const mockedFetch = vi.fn().mockResolvedValueOnce(response) vi.mocked(fetch).mockImplementation(mockedFetch) @@ -393,8 +394,6 @@ describe('pollAppLogs', () => { // Then expect(outputWarnSpy).toHaveBeenCalledWith('Error while polling app logs.') - expect(outputWarnSpy).toHaveBeenCalledWith('Retrying in 5 seconds.') - expect(outputDebugSpy).toHaveBeenCalledWith(expect.stringContaining(`Unhandled bad response: ${response.status}`)) expect(vi.getTimerCount()).toEqual(1) }) }) diff --git a/packages/app/src/cli/services/app-logs/dev/poll-app-logs.ts b/packages/app/src/cli/services/app-logs/dev/poll-app-logs.ts index 60c33a0e500..5d2fe1976d9 100644 --- a/packages/app/src/cli/services/app-logs/dev/poll-app-logs.ts +++ b/packages/app/src/cli/services/app-logs/dev/poll-app-logs.ts @@ -2,7 +2,6 @@ import {writeAppLogsToFile} from './write-app-logs.js' import { POLLING_INTERVAL_MS, POLLING_ERROR_RETRY_INTERVAL_MS, - POLLING_THROTTLE_RETRY_INTERVAL_MS, ONE_MILLION, LOG_TYPE_FUNCTION_RUN, fetchAppLogs, @@ -12,8 +11,9 @@ import { LOG_TYPE_REQUEST_EXECUTION, REQUEST_EXECUTION_IN_BACKGROUND_NO_CACHED_RESPONSE_REASON, REQUEST_EXECUTION_IN_BACKGROUND_CACHE_ABOUT_TO_EXPIRE_REASON, + handleFetchAppLogsError, } from '../utils.js' -import {AppLogData} from '../types.js' +import {AppLogData, ErrorResponse} from '../types.js' import {outputContent, outputDebug, outputToken, outputWarn} from '@shopify/cli-kit/node/output' import {useConcurrentOutputContext} from '@shopify/cli-kit/node/ui/components' import {Writable} from 'stream' @@ -27,37 +27,44 @@ export const pollAppLogs = async ({ stdout: Writable appLogsFetchInput: {jwtToken: string; cursor?: string} apiKey: string - resubscribeCallback: () => Promise + resubscribeCallback: () => Promise }) => { try { - const response = await fetchAppLogs(jwtToken, cursor) - - if (!response.ok) { - if (response.status === 401) { - await resubscribeCallback() - } else if (response.status === 429) { - outputWarn(`Request throttled while polling app logs.`) - outputWarn(`Retrying in ${POLLING_THROTTLE_RETRY_INTERVAL_MS / 1000} seconds.`) - setTimeout(() => { - pollAppLogs({ - stdout, - appLogsFetchInput: { - jwtToken, - cursor: undefined, - }, - apiKey, - resubscribeCallback, - }).catch((error) => { - outputDebug(`Unexpected error during polling: ${error}}\n`) - }) - }, POLLING_THROTTLE_RETRY_INTERVAL_MS) - } else { - throw new Error(`Unhandled bad response: ${response.status}`) + let nextJwtToken = jwtToken + let retryIntervalMs = POLLING_INTERVAL_MS + + const httpResponse = await fetchAppLogs(jwtToken, cursor) + + const response = await httpResponse.json() + const {errors} = response as {errors: string[]} + + if (errors) { + const errorResponse = { + errors: errors.map((error) => ({message: error, status: httpResponse.status})), + } as ErrorResponse + + const result = await handleFetchAppLogsError({ + response: errorResponse, + onThrottle: (retryIntervalMs) => { + outputWarn(`Request throttled while polling app logs.`) + outputWarn(`Retrying in ${retryIntervalMs / 1000} seconds.`) + }, + onUnknownError: (retryIntervalMs) => { + outputWarn(`Error while polling app logs.`) + outputWarn(`Retrying in ${retryIntervalMs / 1000} seconds.`) + }, + onResubscribe: () => { + return resubscribeCallback() + }, + }) + + if (result.nextJwtToken) { + nextJwtToken = result.nextJwtToken } - return + retryIntervalMs = result.retryIntervalMs } - const data = (await response.json()) as { + const data = response as { app_logs?: AppLogData[] cursor?: string errors?: string[] @@ -101,15 +108,15 @@ export const pollAppLogs = async ({ pollAppLogs({ stdout, appLogsFetchInput: { - jwtToken, - cursor: cursorFromResponse, + jwtToken: nextJwtToken, + cursor: cursorFromResponse || cursor, }, apiKey, resubscribeCallback, }).catch((error) => { outputDebug(`Unexpected error during polling: ${error}}\n`) }) - }, POLLING_INTERVAL_MS) + }, retryIntervalMs) // eslint-disable-next-line no-catch-all/no-catch-all } catch (error) { outputWarn(`Error while polling app logs.`) diff --git a/packages/app/src/cli/services/app-logs/logs-command/render-json-logs.test.ts b/packages/app/src/cli/services/app-logs/logs-command/render-json-logs.test.ts new file mode 100644 index 00000000000..b021d0d588a --- /dev/null +++ b/packages/app/src/cli/services/app-logs/logs-command/render-json-logs.test.ts @@ -0,0 +1,79 @@ +import {renderJsonLogs} from './render-json-logs.js' +import {pollAppLogs} from './poll-app-logs.js' +import {handleFetchAppLogsError} from '../utils.js' +import {testDeveloperPlatformClient} from '../../../models/app/app.test-data.js' +import {outputInfo} from '@shopify/cli-kit/node/output' +import {describe, expect, vi, test, beforeEach, afterEach} from 'vitest' + +vi.mock('./poll-app-logs') +vi.mock('../utils', async (importOriginal) => { + const mod = await importOriginal() + return { + ...mod, + fetchAppLogs: vi.fn(), + handleFetchAppLogsError: vi.fn(), + } +}) +vi.mock('@shopify/cli-kit/node/output') + +describe('renderJsonLogs', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.clearAllTimers() + }) + + test('should handle success response correctly', async () => { + const mockSuccessResponse = { + cursor: 'next-cursor', + appLogs: [{message: 'Log 1'}, {message: 'Log 2'}], + } + const pollAppLogsMock = vi.fn().mockResolvedValue(mockSuccessResponse) + vi.mocked(pollAppLogs).mockImplementation(pollAppLogsMock) + + await renderJsonLogs({ + pollOptions: {cursor: 'cursor', filters: {status: undefined, source: undefined}, jwtToken: 'jwtToken'}, + options: { + variables: {shopIds: ['1'], apiKey: 'key', token: 'token'}, + developerPlatformClient: testDeveloperPlatformClient(), + }, + }) + + expect(outputInfo).toHaveBeenNthCalledWith(1, JSON.stringify({message: 'Log 1'})) + expect(outputInfo).toHaveBeenNthCalledWith(2, JSON.stringify({message: 'Log 2'})) + expect(pollAppLogs).toHaveBeenCalled() + expect(vi.getTimerCount()).toEqual(1) + }) + + test('should handle error response and retry as expected', async () => { + const mockErrorResponse = { + errors: [{status: 500, message: 'Server Error'}], + } + const pollAppLogsMock = vi.fn().mockResolvedValue(mockErrorResponse) + vi.mocked(pollAppLogs).mockImplementation(pollAppLogsMock) + const mockRetryInterval = 1000 + const handleFetchAppLogsErrorMock = vi.fn((input) => { + input.onUnknownError(mockRetryInterval) + return new Promise<{retryIntervalMs: number; nextJwtToken: string | null}>((resolve, _reject) => { + resolve({nextJwtToken: 'new-jwt-token', retryIntervalMs: mockRetryInterval}) + }) + }) + vi.mocked(handleFetchAppLogsError).mockImplementation(handleFetchAppLogsErrorMock) + + await renderJsonLogs({ + pollOptions: {cursor: 'cursor', filters: {status: undefined, source: undefined}, jwtToken: 'jwtToken'}, + options: { + variables: {shopIds: [], apiKey: '', token: ''}, + developerPlatformClient: testDeveloperPlatformClient(), + }, + }) + + expect(outputInfo).toHaveBeenCalledWith( + JSON.stringify({message: 'Error while polling app logs.', retry_in_ms: mockRetryInterval}), + ) + expect(pollAppLogs).toHaveBeenCalled() + expect(vi.getTimerCount()).toEqual(1) + }) +}) diff --git a/packages/app/src/cli/services/app-logs/logs-command/render-json-logs.ts b/packages/app/src/cli/services/app-logs/logs-command/render-json-logs.ts new file mode 100644 index 00000000000..74644f04533 --- /dev/null +++ b/packages/app/src/cli/services/app-logs/logs-command/render-json-logs.ts @@ -0,0 +1,59 @@ +import {pollAppLogs} from './poll-app-logs.js' +import {PollOptions, SubscribeOptions, ErrorResponse, SuccessResponse} from '../types.js' +import {POLLING_INTERVAL_MS, handleFetchAppLogsError, subscribeToAppLogs} from '../utils.js' +import {outputInfo} from '@shopify/cli-kit/node/output' + +export async function renderJsonLogs({ + pollOptions: {cursor, filters, jwtToken}, + options: {variables, developerPlatformClient}, +}: { + pollOptions: PollOptions + options: SubscribeOptions +}): Promise { + const response = await pollAppLogs({cursor, filters, jwtToken}) + let retryIntervalMs = POLLING_INTERVAL_MS + let nextJwtToken = jwtToken + + const errorResponse = response as ErrorResponse + + if (errorResponse.errors) { + const result = await handleFetchAppLogsError({ + response: errorResponse, + onThrottle: (retryIntervalMs) => { + outputInfo(JSON.stringify({message: 'Request throttled while polling app logs.', retry_in_ms: retryIntervalMs})) + }, + onUnknownError: (retryIntervalMs) => { + outputInfo(JSON.stringify({message: 'Error while polling app logs.', retry_in_ms: retryIntervalMs})) + }, + onResubscribe: () => { + return subscribeToAppLogs(developerPlatformClient, variables) + }, + }) + + if (result.nextJwtToken) { + nextJwtToken = result.nextJwtToken + } + retryIntervalMs = result.retryIntervalMs + } + + const {cursor: nextCursor, appLogs} = response as SuccessResponse + + if (appLogs) { + appLogs.forEach((log) => { + outputInfo(JSON.stringify(log)) + }) + } + + setTimeout(() => { + renderJsonLogs({ + options: {variables, developerPlatformClient}, + pollOptions: { + jwtToken: nextJwtToken || jwtToken, + cursor: nextCursor || cursor, + filters, + }, + }).catch((error) => { + throw error + }) + }, retryIntervalMs) +} diff --git a/packages/app/src/cli/services/app-logs/logs-command/ui/components/hooks/usePollAppLogs.test.tsx b/packages/app/src/cli/services/app-logs/logs-command/ui/components/hooks/usePollAppLogs.test.tsx index c94ef1a8c5a..c120af4746d 100644 --- a/packages/app/src/cli/services/app-logs/logs-command/ui/components/hooks/usePollAppLogs.test.tsx +++ b/packages/app/src/cli/services/app-logs/logs-command/ui/components/hooks/usePollAppLogs.test.tsx @@ -344,7 +344,7 @@ describe('usePollAppLogs', () => { expect(mockedPollAppLogs).toHaveBeenCalledTimes(1) expect(hook.lastResult?.appLogOutputs).toHaveLength(0) - expect(hook.lastResult?.errors[0]).toEqual('Error Message') + expect(hook.lastResult?.errors[0]).toEqual('Request throttled while polling app logs.') expect(hook.lastResult?.errors[1]).toEqual('Retrying in 60s') expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), POLLING_THROTTLE_RETRY_INTERVAL_MS) @@ -378,7 +378,7 @@ describe('usePollAppLogs', () => { expect(mockedPollAppLogs).toHaveBeenCalledTimes(1) expect(hook.lastResult?.appLogOutputs).toHaveLength(0) - expect(hook.lastResult?.errors[0]).toEqual('Unprocessable') + expect(hook.lastResult?.errors[0]).toEqual('Error while polling app logs') expect(hook.lastResult?.errors[1]).toEqual('Retrying in 5s') expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), POLLING_ERROR_RETRY_INTERVAL_MS) diff --git a/packages/app/src/cli/services/app-logs/logs-command/ui/components/hooks/usePollAppLogs.ts b/packages/app/src/cli/services/app-logs/logs-command/ui/components/hooks/usePollAppLogs.ts index 65e9005cfe4..0eb6dbff161 100644 --- a/packages/app/src/cli/services/app-logs/logs-command/ui/components/hooks/usePollAppLogs.ts +++ b/packages/app/src/cli/services/app-logs/logs-command/ui/components/hooks/usePollAppLogs.ts @@ -1,8 +1,6 @@ import { - POLLING_ERROR_RETRY_INTERVAL_MS, ONE_MILLION, POLLING_INTERVAL_MS, - POLLING_THROTTLE_RETRY_INTERVAL_MS, parseFunctionRunPayload, LOG_TYPE_FUNCTION_RUN, LOG_TYPE_RESPONSE_FROM_CACHE, @@ -11,6 +9,7 @@ import { parseNetworkAccessRequestExecutionInBackgroundPayload, LOG_TYPE_REQUEST_EXECUTION, parseNetworkAccessRequestExecutedPayload, + handleFetchAppLogsError, } from '../../../../utils.js' import {ErrorResponse, SuccessResponse, AppLogOutput, PollFilters, AppLogPayload} from '../../../../types.js' import {pollAppLogs} from '../../../poll-app-logs.js' @@ -28,23 +27,30 @@ export function usePollAppLogs({initialJwt, filters, resubscribeCallback}: UsePo useEffect(() => { const poll = async ({jwtToken, cursor, filters}: {jwtToken: string; cursor?: string; filters: PollFilters}) => { - let nextInterval = POLLING_INTERVAL_MS let nextJwtToken = jwtToken + let retryIntervalMs = POLLING_INTERVAL_MS const response = await pollAppLogs({jwtToken, cursor, filters}) - const {errors} = response as ErrorResponse + const errorResponse = response as ErrorResponse - if (errors && errors.length > 0) { - const errorsStrings = errors.map((error) => error.message) - if (errors.some((error) => error.status === 429)) { - setErrors([...errorsStrings, `Retrying in ${POLLING_THROTTLE_RETRY_INTERVAL_MS / 1000}s`]) - nextInterval = POLLING_THROTTLE_RETRY_INTERVAL_MS - } else if (errors.some((error) => error.status === 401)) { - nextJwtToken = await resubscribeCallback() - } else { - setErrors([...errorsStrings, `Retrying in ${POLLING_ERROR_RETRY_INTERVAL_MS / 1000}s`]) - nextInterval = POLLING_ERROR_RETRY_INTERVAL_MS + if (errorResponse.errors) { + const result = await handleFetchAppLogsError({ + response: errorResponse, + onThrottle: (retryIntervalMs) => { + setErrors(['Request throttled while polling app logs.', `Retrying in ${retryIntervalMs / 1000}s`]) + }, + onUnknownError: (retryIntervalMs) => { + setErrors(['Error while polling app logs', `Retrying in ${retryIntervalMs / 1000}s`]) + }, + onResubscribe: () => { + return resubscribeCallback() + }, + }) + + if (result.nextJwtToken) { + nextJwtToken = result.nextJwtToken } + retryIntervalMs = result.retryIntervalMs } else { setErrors([]) } @@ -97,7 +103,7 @@ export function usePollAppLogs({initialJwt, filters, resubscribeCallback}: UsePo poll({jwtToken: nextJwtToken, cursor: nextCursor || cursor, filters}).catch((error) => { throw error }) - }, nextInterval) + }, retryIntervalMs) } poll({jwtToken: initialJwt, cursor: '', filters}).catch((error) => { diff --git a/packages/app/src/cli/services/app-logs/utils.ts b/packages/app/src/cli/services/app-logs/utils.ts index 70490a1255c..693a4da4230 100644 --- a/packages/app/src/cli/services/app-logs/utils.ts +++ b/packages/app/src/cli/services/app-logs/utils.ts @@ -4,6 +4,7 @@ import { NetworkAccessRequestExecutedLog, NetworkAccessRequestExecutionInBackgroundLog, NetworkAccessResponseFromCacheLog, + ErrorResponse, } from './types.js' import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' import {AppLogsSubscribeVariables} from '../../api/graphql/subscribe_to_app_logs.js' @@ -128,6 +129,38 @@ export const fetchAppLogs = async ( }) } +interface FetchAppLogsErrorOptions { + response: ErrorResponse + onThrottle: (retryIntervalMs: number) => void + onUnknownError: (retryIntervalMs: number) => void + onResubscribe: () => Promise +} + +export const handleFetchAppLogsError = async ( + input: FetchAppLogsErrorOptions, +): Promise<{retryIntervalMs: number; nextJwtToken: string | null}> => { + const {errors} = input.response + + let retryIntervalMs = POLLING_INTERVAL_MS + let nextJwtToken = null + + if (errors.length > 0) { + outputDebug(`Errors: ${errors.map((error) => error.message).join(', ')}`) + + if (errors.some((error) => error.status === 401)) { + nextJwtToken = await input.onResubscribe() + } else if (errors.some((error) => error.status === 429)) { + retryIntervalMs = POLLING_THROTTLE_RETRY_INTERVAL_MS + input.onThrottle(retryIntervalMs) + } else { + retryIntervalMs = POLLING_ERROR_RETRY_INTERVAL_MS + input.onUnknownError(retryIntervalMs) + } + } + + return {retryIntervalMs, nextJwtToken} +} + export const subscribeToAppLogs = async ( developerPlatformClient: DeveloperPlatformClient, variables: AppLogsSubscribeVariables, diff --git a/packages/app/src/cli/services/context.test.ts b/packages/app/src/cli/services/context.test.ts index 4629b24d5b6..c263e7b769b 100644 --- a/packages/app/src/cli/services/context.test.ts +++ b/packages/app/src/cli/services/context.test.ts @@ -890,6 +890,32 @@ api_version = "2023-04" expect(link).toBeCalled() }) + describe('when --json is in argv', () => { + let originalArgv: string[] + + beforeEach(() => { + originalArgv = process.argv + }) + + // Restore the original process.argv + afterEach(() => { + process.argv = originalArgv + }) + + test('Does not display used dev values when using json output', async () => { + vi.mocked(getCachedAppInfo).mockReturnValue({...CACHED1, previousAppId: APP1.apiKey}) + vi.mocked(fetchAppDetailsFromApiKey).mockResolvedValueOnce(APP1) + vi.mocked(fetchStoreByDomain).mockResolvedValue({organization: ORG1, store: STORE1}) + + // When + const options = devOptions() + process.argv = ['', '', '--json'] + await ensureDevContext(options) + + expect(renderInfo).not.toBeCalled() + }) + }) + test('links app if no app configs exist & cache has a current config file defined', async () => { await inTemporaryDirectory(async (tmp) => { // Given diff --git a/packages/app/src/cli/services/context.ts b/packages/app/src/cli/services/context.ts index 273d48d31ca..c809a745b11 100644 --- a/packages/app/src/cli/services/context.ts +++ b/packages/app/src/cli/services/context.ts @@ -40,9 +40,8 @@ import {partnersFqdn} from '@shopify/cli-kit/node/context/fqdn' import {AbortError} from '@shopify/cli-kit/node/error' import {outputContent} from '@shopify/cli-kit/node/output' import {getOrganization} from '@shopify/cli-kit/node/environment' -import {basename, joinPath} from '@shopify/cli-kit/node/path' +import {basename, joinPath, sniffForJson} from '@shopify/cli-kit/node/path' import {glob} from '@shopify/cli-kit/node/fs' -import {sniffForJson} from '@shopify/cli-kit/node/path' export const InvalidApiKeyErrorMessage = (apiKey: string) => { return { diff --git a/packages/app/src/cli/services/dev/processes/app-logs-polling.ts b/packages/app/src/cli/services/dev/processes/app-logs-polling.ts index 7816a99906f..9138113918c 100644 --- a/packages/app/src/cli/services/dev/processes/app-logs-polling.ts +++ b/packages/app/src/cli/services/dev/processes/app-logs-polling.ts @@ -45,7 +45,7 @@ export async function setupAppLogsPollingProcess({ } export const subscribeAndStartPolling: DevProcessFunction = async ( - {stdout, stderr, abortSignal}, + {stdout, stderr: _stderr, abortSignal: _abortSignal}, {developerPlatformClient, appLogsSubscribeVariables}, ) => { try { @@ -59,10 +59,7 @@ export const subscribeAndStartPolling: DevProcessFunction { - return subscribeAndStartPolling( - {stdout, stderr, abortSignal}, - {developerPlatformClient, appLogsSubscribeVariables}, - ) + return subscribeToAppLogs(developerPlatformClient, appLogsSubscribeVariables) }, }) // eslint-disable-next-line no-catch-all/no-catch-all,no-empty diff --git a/packages/app/src/cli/services/logs.test.ts b/packages/app/src/cli/services/logs.test.ts new file mode 100644 index 00000000000..f5114908f47 --- /dev/null +++ b/packages/app/src/cli/services/logs.test.ts @@ -0,0 +1,83 @@ +import {logs} from './logs.js' +import {subscribeToAppLogs} from './app-logs/utils.js' +import {ensureDevContext} from './context.js' +import * as renderLogs from './app-logs/logs-command/ui.js' +import * as renderJsonLogs from './app-logs/logs-command/render-json-logs.js' +import {loadAppConfiguration} from '../models/app/loader.js' +import {buildVersionedAppSchema, testApp, testOrganizationApp} from '../models/app/app.test-data.js' +import {describe, test, vi, expect} from 'vitest' + +vi.mock('../models/app/loader.js') +vi.mock('./context.js') +vi.mock('./app-logs/logs-command/ui.js') +vi.mock('./app-logs/logs-command/render-json-logs.js') +vi.mock('./app-logs/utils.js') + +describe('logs', () => { + test('should call json handler when format is json', async () => { + // Given + await setupDevContext() + const spy = vi.spyOn(renderJsonLogs, 'renderJsonLogs') + + // When + await logs({ + reset: false, + format: 'json', + directory: 'directory', + apiKey: 'api-key', + storeFqdn: 'store-fqdn', + source: 'source', + status: 'status', + configName: 'config-name', + userProvidedConfigName: 'user-provided-config-name', + }) + + // Then + expect(spy).toHaveBeenCalled() + }) + + test('should call text handler when format is texxt', async () => { + // Given + await setupDevContext() + const spy = vi.spyOn(renderLogs, 'renderLogs') + + // When + await logs({ + reset: false, + format: 'text', + apiKey: 'api-key', + directory: 'directory', + storeFqdn: 'store-fqdn', + source: 'source', + status: 'status', + configName: 'config-name', + userProvidedConfigName: 'user-provided-config-name', + }) + + // Then + expect(spy).toHaveBeenCalled() + }) +}) + +async function setupDevContext() { + const {schema: configSchema} = await buildVersionedAppSchema() + vi.mocked(loadAppConfiguration).mockResolvedValue({ + directory: '/app', + configuration: { + path: '/app/shopify.app.toml', + scopes: 'read_products', + }, + configSchema, + specifications: [], + remoteFlags: [], + }) + vi.mocked(ensureDevContext).mockResolvedValue({ + localApp: testApp(), + remoteApp: testOrganizationApp(), + remoteAppUpdated: false, + updateURLs: false, + storeFqdn: 'store-fqdn', + storeId: '1', + }) + vi.mocked(subscribeToAppLogs).mockResolvedValue('jwt-token') +} diff --git a/packages/app/src/cli/services/logs.ts b/packages/app/src/cli/services/logs.ts index f2444b75da5..f94d7472814 100644 --- a/packages/app/src/cli/services/logs.ts +++ b/packages/app/src/cli/services/logs.ts @@ -1,27 +1,12 @@ import {DevContextOptions, ensureDevContext} from './context.js' import {renderLogs} from './app-logs/logs-command/ui.js' import {subscribeToAppLogs} from './app-logs/utils.js' -import {selectDeveloperPlatformClient, DeveloperPlatformClient} from '../utilities/developer-platform-client.js' -import {loadAppConfiguration} from '../models/app/loader.js' +import {renderJsonLogs} from './app-logs/logs-command/render-json-logs.js' import {AppInterface} from '../models/app/app.js' -import {pollAppLogs} from './app-logs/logs-command/poll-app-logs.js' -import {PollOptions, SubscribeOptions} from './app-logs/types.js' -import { - POLLING_ERROR_RETRY_INTERVAL_MS, - ONE_MILLION, - POLLING_INTERVAL_MS, - POLLING_THROTTLE_RETRY_INTERVAL_MS, - parseFunctionRunPayload, - LOG_TYPE_FUNCTION_RUN, - LOG_TYPE_RESPONSE_FROM_CACHE, - parseNetworkAccessResponseFromCachePayload, - LOG_TYPE_REQUEST_EXECUTION_IN_BACKGROUND, - parseNetworkAccessRequestExecutionInBackgroundPayload, - LOG_TYPE_REQUEST_EXECUTION, - parseNetworkAccessRequestExecutedPayload, -} from './app-logs/utils.js' -import {ErrorResponse, SuccessResponse, AppLogOutput, PollFilters, AppLogPayload} from './app-logs/types.js' -import {outputInfo} from '@shopify/cli-kit/node/output' +import {loadAppConfiguration} from '../models/app/loader.js' +import {selectDeveloperPlatformClient, DeveloperPlatformClient} from '../utilities/developer-platform-client.js' + +export type Format = 'json' | 'text' interface LogsOptions { directory: string @@ -32,6 +17,7 @@ interface LogsOptions { status?: string configName?: string userProvidedConfigName?: string + format: Format } export async function logs(commandOptions: LogsOptions) { @@ -55,21 +41,23 @@ export async function logs(commandOptions: LogsOptions) { filters, } - await renderJsonLogs({ - options: { - variables, - developerPlatformClient: logsConfig.developerPlatformClient, - }, - pollOptions, - }) - - // await renderLogs({ - // options: { - // variables, - // developerPlatformClient: logsConfig.developerPlatformClient, - // }, - // pollOptions, - // }) + if (commandOptions.format === 'json') { + await renderJsonLogs({ + options: { + variables, + developerPlatformClient: logsConfig.developerPlatformClient, + }, + pollOptions, + }) + } else { + await renderLogs({ + options: { + variables, + developerPlatformClient: logsConfig.developerPlatformClient, + }, + pollOptions, + }) + } } async function prepareForLogs(commandOptions: LogsOptions): Promise<{ @@ -97,55 +85,3 @@ async function prepareForLogs(commandOptions: LogsOptions): Promise<{ localApp, } } - -async function renderJsonLogs({ - pollOptions: {cursor, filters, jwtToken}, - options: {variables, developerPlatformClient}, -}: { - pollOptions: PollOptions - options: SubscribeOptions -}): Promise { - const response = await pollAppLogs({cursor, filters, jwtToken}) - let nextInterval = POLLING_INTERVAL_MS - let nextJwtToken = jwtToken - - const {errors} = response as ErrorResponse - - if (errors && errors.length > 0) { - if (errors.some((error) => error.status === 401)) { - const nextJwtToken = await subscribeToAppLogs(developerPlatformClient, variables) - } else if (errors.some((error) => error.status === 429)) { - nextInterval = POLLING_THROTTLE_RETRY_INTERVAL_MS - } else { - nextInterval = POLLING_ERROR_RETRY_INTERVAL_MS - - outputInfo( - JSON.stringify({ - errors: errors, - retrying_in_ms: nextInterval, - }), - ) - } - } - - const {cursor: nextCursor, appLogs} = response as SuccessResponse - - if (appLogs) { - appLogs.forEach((log) => { - outputInfo(JSON.stringify(log)) - }) - } - - setTimeout(() => { - renderJsonLogs({ - options: {variables: variables, developerPlatformClient: developerPlatformClient}, - pollOptions: { - jwtToken: nextJwtToken || jwtToken, - cursor: nextCursor || cursor, - filters, - }, - }).catch((error) => { - throw error - }) - }, nextInterval) -} diff --git a/packages/cli/README.md b/packages/cli/README.md index 10ad1777a5c..429d6d41cae 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -763,8 +763,8 @@ USAGE [--entry ] [--lockfile-check] [--path ] [--sourcemap] [--watch] FLAGS - --[no-]bundle-stats [Classic Remix Compiler] Show a bundle size summary after building. Defaults to true, - use `--no-bundle-stats` to disable. + --[no-]bundle-stats Show a bundle size summary after building. Defaults to true, use `--no-bundle-stats` to + disable. --codegen Automatically generates GraphQL types for your project’s Storefront API queries. --codegen-config-path= Specifies a path to a codegen configuration file. Defaults to `/codegen.ts` if this file exists. @@ -869,38 +869,44 @@ Builds and deploys a Hydrogen storefront to Oxygen. ``` USAGE - $ shopify hydrogen deploy [--auth-bypass-token] [--build-command ] [--entry ] [--env | - --env-branch ] [--env-file ] [-f] [--json-output] [--lockfile-check] [--metadata-description ] - [--metadata-user ] [--no-verify] [--path ] [--preview] [-s ] [-t ] - -FLAGS - -f, --force Forces a deployment to proceed if there are uncommited changes in its Git - repository. - -s, --shop= Shop URL. It can be the shop prefix (janes-apparel) or the full myshopify.com URL - (janes-apparel.myshopify.com, https://janes-apparel.myshopify.com). - -t, --token= Oxygen deployment token. Defaults to the linked storefront's token if available. - --auth-bypass-token Generate an authentication bypass token, which can be used to perform end-to-end - tests against the deployment. - --build-command= Specify a build command to run before deploying. If not specified, `shopify - hydrogen build` will be used. - --entry= Entry file for the worker. Defaults to `./server`. - --env= Specifies the environment to perform the operation using its handle. Fetch the - handle using the `env list` command. - --env-branch= Specifies the environment to perform the operation using its Git branch name. - --env-file= Path to an environment file to override existing environment variables for the - deployment. - --[no-]json-output Create a JSON file containing the deployment details in CI environments. Defaults - to true, use `--no-json-output` to disable. - --[no-]lockfile-check Checks that there is exactly one valid lockfile in the project. Defaults to - `true`. Deactivate with `--no-lockfile-check`. - --metadata-description= Description of the changes in the deployment. Defaults to the commit message of - the latest commit if there are no uncommited changes. - --metadata-user= User that initiated the deployment. Will be saved and displayed in the Shopify - admin - --no-verify Skip the routability verification step after deployment. - --path= The path to the directory of the Hydrogen storefront. Defaults to the current - directory where the command is run. - --preview Deploys to the Preview environment. Overrides --env-branch and Git metadata. + $ shopify hydrogen deploy [--auth-bypass-token-duration --auth-bypass-token] [--build-command ] + [--entry ] [--env | --env-branch ] [--env-file ] [-f] [--json-output] + [--lockfile-check] [--metadata-description ] [--metadata-user ] [--no-verify] [--path ] + [--preview] [-s ] [-t ] + +FLAGS + -f, --force Forces a deployment to proceed if there are uncommited changes in its Git + repository. + -s, --shop= Shop URL. It can be the shop prefix (janes-apparel) or the full + myshopify.com URL (janes-apparel.myshopify.com, + https://janes-apparel.myshopify.com). + -t, --token= Oxygen deployment token. Defaults to the linked storefront's token if + available. + --auth-bypass-token Generate an authentication bypass token, which can be used to perform + end-to-end tests against the deployment. + --auth-bypass-token-duration= Specify the duration (in hours) up to 12 hours for the authentication bypass + token. Defaults to `2` + --build-command= Specify a build command to run before deploying. If not specified, `shopify + hydrogen build` will be used. + --entry= Entry file for the worker. Defaults to `./server`. + --env= Specifies the environment to perform the operation using its handle. Fetch + the handle using the `env list` command. + --env-branch= Specifies the environment to perform the operation using its Git branch + name. + --env-file= Path to an environment file to override existing environment variables for + the deployment. + --[no-]json-output Create a JSON file containing the deployment details in CI environments. + Defaults to true, use `--no-json-output` to disable. + --[no-]lockfile-check Checks that there is exactly one valid lockfile in the project. Defaults to + `true`. Deactivate with `--no-lockfile-check`. + --metadata-description= Description of the changes in the deployment. Defaults to the commit message + of the latest commit if there are no uncommited changes. + --metadata-user= User that initiated the deployment. Will be saved and displayed in the + Shopify admin + --no-verify Skip the routability verification step after deployment. + --path= The path to the directory of the Hydrogen storefront. Defaults to the + current directory where the command is run. + --preview Deploys to the Preview environment. Overrides --env-branch and Git metadata. DESCRIPTION Builds and deploys a Hydrogen storefront to Oxygen. @@ -1053,7 +1059,7 @@ Creates a new Hydrogen storefront. ``` USAGE $ shopify hydrogen init [-f] [--git] [--install-deps] [--language ] [--markets ] [--mock-shop] - [--path ] [--quickstart] [--routes] [--shortcut] [--template ] + [--path ] [--quickstart] [--routes] [--shortcut] [--styling ] [--template ] FLAGS -f, --force Overwrites the destination directory and files if they already exist. @@ -1070,6 +1076,8 @@ FLAGS --[no-]routes Generate routes for all pages. --[no-]shortcut Creates a global h2 shortcut for Shopify CLI using shell aliases. Deactivate with `--no-shortcut`. + --styling= Sets the styling strategy to use. One of `tailwind`, `vanilla-extract`, `css-modules`, + `postcss`, `none`. --template= Scaffolds project based on an existing template or example from the Hydrogen repository. DESCRIPTION @@ -1208,8 +1216,8 @@ USAGE $ shopify hydrogen setup css [STRATEGY] [-f] [--install-deps] [--path ] ARGUMENTS - STRATEGY (tailwind|css-modules|vanilla-extract|postcss) The CSS strategy to setup. One of - tailwind,css-modules,vanilla-extract,postcss + STRATEGY (tailwind|vanilla-extract|css-modules|postcss) The CSS strategy to setup. One of + tailwind,vanilla-extract,css-modules,postcss FLAGS -f, --force Overwrites the destination directory and files if they already exist. diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 44d3e641b95..83b3f958022 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -2522,7 +2522,7 @@ "flags": { "bundle-stats": { "allowNo": true, - "description": "[Classic Remix Compiler] Show a bundle size summary after building. Defaults to true, use `--no-bundle-stats` to disable.", + "description": "Show a bundle size summary after building. Defaults to true, use `--no-bundle-stats` to disable.", "name": "bundle-stats", "type": "boolean" }, @@ -2815,10 +2815,23 @@ "auth-bypass-token": { "allowNo": false, "description": "Generate an authentication bypass token, which can be used to perform end-to-end tests against the deployment.", + "env": "AUTH_BYPASS_TOKEN", "name": "auth-bypass-token", "required": false, "type": "boolean" }, + "auth-bypass-token-duration": { + "dependsOn": [ + "auth-bypass-token" + ], + "description": "Specify the duration (in hours) up to 12 hours for the authentication bypass token. Defaults to `2`", + "env": "AUTH_BYPASS_TOKEN_DURATION", + "hasDynamicHelp": false, + "multiple": false, + "name": "auth-bypass-token-duration", + "required": false, + "type": "option" + }, "build-command": { "description": "Specify a build command to run before deploying. If not specified, `shopify hydrogen build` will be used.", "hasDynamicHelp": false, @@ -3539,6 +3552,14 @@ "name": "shortcut", "type": "boolean" }, + "styling": { + "description": "Sets the styling strategy to use. One of `tailwind`, `vanilla-extract`, `css-modules`, `postcss`, `none`.", + "env": "SHOPIFY_HYDROGEN_FLAG_STYLING", + "hasDynamicHelp": false, + "multiple": false, + "name": "styling", + "type": "option" + }, "template": { "description": "Scaffolds project based on an existing template or example from the Hydrogen repository.", "env": "SHOPIFY_HYDROGEN_FLAG_TEMPLATE", @@ -3907,12 +3928,12 @@ ], "args": { "strategy": { - "description": "The CSS strategy to setup. One of tailwind,css-modules,vanilla-extract,postcss", + "description": "The CSS strategy to setup. One of tailwind,vanilla-extract,css-modules,postcss", "name": "strategy", "options": [ "tailwind", - "css-modules", "vanilla-extract", + "css-modules", "postcss" ] }