Skip to content

Commit 05afd3f

Browse files
Revert "Fetch notifications in background"
1 parent 5159ef6 commit 05afd3f

File tree

6 files changed

+32
-126
lines changed

6 files changed

+32
-126
lines changed

.changeset/violet-carrots-argue.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/cli-kit/src/public/node/hooks/postrun.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@ import {reportAnalyticsEvent} from '../analytics.js'
33
import {outputDebug} from '../../../public/node/output.js'
44
import BaseCommand from '../base-command.js'
55
import * as metadata from '../../../public/node/metadata.js'
6-
import {fetchNotificationsInBackground} from '../notifications-system.js'
76
import {Command, Hook} from '@oclif/core'
87

98
// This hook is called after each successful command run. More info: https://oclif.io/docs/hooks
109
export const hook: Hook.Postrun = async ({config, Command}) => {
1110
await detectStopCommand(Command as unknown as typeof Command)
1211
await reportAnalyticsEvent({config, exitMode: 'ok'})
13-
if (!Command.hidden) fetchNotificationsInBackground(Command.id)
1412
deprecationsHook(Command)
1513

1614
const command = Command.id.replace(/:/g, ' ')
@@ -28,10 +26,10 @@ async function detectStopCommand(commandClass: Command.Class | typeof BaseComman
2826
const stopCommand = (commandClass as typeof BaseCommand).analyticsStopCommand()
2927
if (stopCommand) {
3028
const {commandStartOptions} = metadata.getAllSensitiveMetadata()
31-
if (!commandStartOptions) return
3229
await metadata.addSensitiveMetadata(() => ({
3330
commandStartOptions: {
34-
...commandStartOptions,
31+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
32+
...commandStartOptions!,
3533
startTime: currentTime,
3634
startCommand: stopCommand,
3735
},

packages/cli-kit/src/public/node/notifications-system.test.ts

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
1-
import {
2-
Notification,
3-
fetchNotificationsInBackground,
4-
filterNotifications,
5-
showNotificationsIfNeeded,
6-
} from './notifications-system.js'
1+
import {Notification, filterNotifications, showNotificationsIfNeeded} from './notifications-system.js'
72
import {renderError, renderInfo, renderWarning} from './ui.js'
83
import {sniffForJson} from './path.js'
9-
import {exec} from './system.js'
10-
import {cacheRetrieve} from '../../private/node/conf-store.js'
4+
import {cacheRetrieve, cacheRetrieveOrRepopulate} from '../../private/node/conf-store.js'
115
import {afterEach, describe, expect, test, vi} from 'vitest'
126

137
vi.mock('./ui.js')
148
vi.mock('../../private/node/conf-store.js')
159
vi.mock('./path.js')
16-
vi.mock('./system.js')
1710

1811
const betweenVersins1and2: Notification = {
1912
id: 'betweenVersins1and2',
@@ -340,7 +333,7 @@ describe('showNotificationsIfNeeded', () => {
340333
test('an info notification triggers a renderInfo call', async () => {
341334
// Given
342335
const notifications = [infoNotification]
343-
vi.mocked(cacheRetrieve).mockReturnValue({value: JSON.stringify({notifications}), timestamp: 0})
336+
vi.mocked(cacheRetrieveOrRepopulate).mockResolvedValue(JSON.stringify({notifications}))
344337

345338
// When
346339
await showNotificationsIfNeeded(undefined, {SHOPIFY_UNIT_TEST: 'false'})
@@ -352,7 +345,7 @@ describe('showNotificationsIfNeeded', () => {
352345
test('a warning notification triggers a renderWarning call', async () => {
353346
// Given
354347
const notifications = [warningNotification]
355-
vi.mocked(cacheRetrieve).mockReturnValue({value: JSON.stringify({notifications}), timestamp: 0})
348+
vi.mocked(cacheRetrieveOrRepopulate).mockResolvedValue(JSON.stringify({notifications}))
356349

357350
// When
358351
await showNotificationsIfNeeded(undefined, {SHOPIFY_UNIT_TEST: 'false'})
@@ -364,7 +357,7 @@ describe('showNotificationsIfNeeded', () => {
364357
test('an error notification triggers a renderError call and throws an error', async () => {
365358
// Given
366359
const notifications = [errorNotification]
367-
vi.mocked(cacheRetrieve).mockReturnValue({value: JSON.stringify({notifications}), timestamp: 0})
360+
vi.mocked(cacheRetrieveOrRepopulate).mockResolvedValue(JSON.stringify({notifications}))
368361

369362
// When
370363
await expect(showNotificationsIfNeeded(undefined, {SHOPIFY_UNIT_TEST: 'false'})).rejects.toThrowError()
@@ -376,7 +369,7 @@ describe('showNotificationsIfNeeded', () => {
376369
test('notifications are skipped on CI', async () => {
377370
// Given
378371
const notifications = [infoNotification]
379-
vi.mocked(cacheRetrieve).mockReturnValue({value: JSON.stringify({notifications}), timestamp: 0})
372+
vi.mocked(cacheRetrieveOrRepopulate).mockResolvedValue(JSON.stringify({notifications}))
380373

381374
// When
382375
await showNotificationsIfNeeded(undefined, {SHOPIFY_UNIT_TEST: 'false', CI: 'true'})
@@ -388,7 +381,7 @@ describe('showNotificationsIfNeeded', () => {
388381
test('notifications are skipped on tests', async () => {
389382
// Given
390383
const notifications = [infoNotification]
391-
vi.mocked(cacheRetrieve).mockReturnValue({value: JSON.stringify({notifications}), timestamp: 0})
384+
vi.mocked(cacheRetrieveOrRepopulate).mockResolvedValue(JSON.stringify({notifications}))
392385

393386
// When
394387
await showNotificationsIfNeeded(undefined, {SHOPIFY_UNIT_TEST: 'true'})
@@ -400,7 +393,7 @@ describe('showNotificationsIfNeeded', () => {
400393
test('notifications are skipped when using --json flag', async () => {
401394
// Given
402395
const notifications = [infoNotification]
403-
vi.mocked(cacheRetrieve).mockReturnValue({value: JSON.stringify({notifications}), timestamp: 0})
396+
vi.mocked(cacheRetrieveOrRepopulate).mockResolvedValue(JSON.stringify({notifications}))
404397
vi.mocked(sniffForJson).mockReturnValue(true)
405398

406399
// When
@@ -413,7 +406,7 @@ describe('showNotificationsIfNeeded', () => {
413406
test('notifications are skipped when using SHOPIFY_FLAG_JSON', async () => {
414407
// Given
415408
const notifications = [infoNotification]
416-
vi.mocked(cacheRetrieve).mockReturnValue({value: JSON.stringify({notifications}), timestamp: 0})
409+
vi.mocked(cacheRetrieveOrRepopulate).mockResolvedValue(JSON.stringify({notifications}))
417410

418411
// When
419412
await showNotificationsIfNeeded(undefined, {SHOPIFY_UNIT_TEST: 'false', SHOPIFY_FLAG_JSON: 'true'})
@@ -422,33 +415,3 @@ describe('showNotificationsIfNeeded', () => {
422415
expect(renderInfo).not.toHaveBeenCalled()
423416
})
424417
})
425-
426-
describe('fetchNotificationsInBackground', () => {
427-
test('calls the expected Shopify binary for global installation', async () => {
428-
// Given / When
429-
fetchNotificationsInBackground('theme:init', ['shopify', 'theme', 'init'], {SHOPIFY_UNIT_TEST: 'false'})
430-
431-
// Then
432-
expect(exec).toHaveBeenCalledWith('shopify', ['notifications', 'list'], expect.anything())
433-
})
434-
435-
test('calls the expected Shopify binary for local installation', async () => {
436-
// Given / When
437-
fetchNotificationsInBackground('theme:init', ['npm', 'run', 'shopify', 'theme', 'init'], {
438-
SHOPIFY_UNIT_TEST: 'false',
439-
})
440-
441-
// Then
442-
expect(exec).toHaveBeenCalledWith('npm', ['run', 'shopify', 'notifications', 'list'], expect.anything())
443-
})
444-
445-
test('calls the expected Shopify binary for dev environment', async () => {
446-
// Given / When
447-
fetchNotificationsInBackground('theme:init', ['node', 'packages/cli/bin/dev.js', 'theme', 'init'], {
448-
SHOPIFY_UNIT_TEST: 'false',
449-
})
450-
451-
// Then
452-
expect(exec).toHaveBeenCalledWith('node', ['packages/cli/bin/dev.js', 'notifications', 'list'], expect.anything())
453-
})
454-
})

packages/cli-kit/src/public/node/notifications-system.ts

Lines changed: 17 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@ import {outputDebug} from './output.js'
55
import {zod} from './schema.js'
66
import {AbortSilentError} from './error.js'
77
import {isTruthy} from './context/utilities.js'
8-
import {exec} from './system.js'
98
import {jsonOutputEnabled} from './environment.js'
109
import {CLI_KIT_VERSION} from '../common/version.js'
11-
import {NotificationKey, NotificationsKey, cacheRetrieve, cacheStore} from '../../private/node/conf-store.js'
10+
import {
11+
NotificationKey,
12+
NotificationsKey,
13+
cacheRetrieve,
14+
cacheRetrieveOrRepopulate,
15+
cacheStore,
16+
} from '../../private/node/conf-store.js'
1217
import {fetch} from '@shopify/cli-kit/node/http'
1318

1419
const URL = 'https://cdn.shopify.com/static/cli/notifications.json'
15-
const EMPTY_CACHE_MESSAGE = 'Cache is empty'
20+
const CACHE_DURATION_IN_MS = 3600 * 1000
1621

1722
function url(): string {
1823
return process.env.SHOPIFY_CLI_NOTIFICATIONS_URL ?? URL
@@ -55,7 +60,7 @@ export async function showNotificationsIfNeeded(
5560
environment: NodeJS.ProcessEnv = process.env,
5661
): Promise<void> {
5762
try {
58-
if (skipNotifications(environment) || jsonOutputEnabled(environment)) return
63+
if (skipNotifications(environment)) return
5964

6065
const notifications = await getNotifications()
6166
const commandId = getCurrentCommandId()
@@ -67,15 +72,14 @@ export async function showNotificationsIfNeeded(
6772
if (error.message === 'abort') throw new AbortSilentError()
6873
const errorMessage = `Error retrieving notifications: ${error.message}`
6974
outputDebug(errorMessage)
70-
if (error.message === EMPTY_CACHE_MESSAGE) return
7175
// This is very prone to becoming a circular dependency, so we import it dynamically
7276
const {sendErrorToBugsnag} = await import('./error-handler.js')
7377
await sendErrorToBugsnag(errorMessage, 'unexpected_error')
7478
}
7579
}
7680

77-
function skipNotifications(environment: NodeJS.ProcessEnv = process.env): boolean {
78-
return isTruthy(environment.CI) || isTruthy(environment.SHOPIFY_UNIT_TEST)
81+
function skipNotifications(environment: NodeJS.ProcessEnv): boolean {
82+
return isTruthy(environment.CI) || isTruthy(environment.SHOPIFY_UNIT_TEST) || jsonOutputEnabled(environment)
7983
}
8084

8185
/**
@@ -109,70 +113,25 @@ async function renderNotifications(notifications: Notification[]) {
109113
}
110114

111115
/**
112-
* Get notifications list from cache, that is updated in the background from bin/fetch-notifications.json.
116+
* Get notifications list from cache (refreshed every hour) or fetch it if not present.
113117
*
114118
* @returns A Notifications object.
115119
*/
116120
export async function getNotifications(): Promise<Notifications> {
117121
const cacheKey: NotificationsKey = `notifications-${url()}`
118-
const rawNotifications = cacheRetrieve(cacheKey)?.value as unknown as string
119-
if (!rawNotifications) throw new Error(EMPTY_CACHE_MESSAGE)
122+
const rawNotifications = await cacheRetrieveOrRepopulate(cacheKey, fetchNotifications, CACHE_DURATION_IN_MS)
120123
const notifications: object = JSON.parse(rawNotifications)
121124
return NotificationsSchema.parse(notifications)
122125
}
123126

124127
/**
125-
* Fetch notifications from the CDN and chache them.
126-
*
127-
* @returns A string with the notifications.
128+
* Fetch notifications from GitHub.
128129
*/
129-
export async function fetchNotifications(): Promise<Notifications> {
130-
outputDebug(`Fetching notifications...`)
130+
async function fetchNotifications(): Promise<string> {
131+
outputDebug(`No cached notifications found. Fetching them...`)
131132
const response = await fetch(url(), {signal: AbortSignal.timeout(3 * 1000)})
132133
if (response.status !== 200) throw new Error(`Failed to fetch notifications: ${response.statusText}`)
133-
const rawNotifications = await response.text()
134-
const notifications: object = JSON.parse(rawNotifications)
135-
const result = NotificationsSchema.parse(notifications)
136-
await cacheNotifications(rawNotifications)
137-
return result
138-
}
139-
140-
/**
141-
* Store the notifications in the cache.
142-
*
143-
* @param notifications - String with the notifications to cache.
144-
* @returns A Notifications object.
145-
*/
146-
async function cacheNotifications(notifications: string): Promise<void> {
147-
cacheStore(`notifications-${url()}`, notifications)
148-
outputDebug(`Notifications from ${url()} stored in the cache`)
149-
}
150-
151-
/**
152-
* Fetch notifications in background as a detached process.
153-
*
154-
* @param currentCommand - The current Shopify command being run.
155-
* @param argv - The arguments passed to the current process.
156-
* @param environment - Process environment variables.
157-
*/
158-
export function fetchNotificationsInBackground(
159-
currentCommand: string,
160-
argv = process.argv,
161-
environment: NodeJS.ProcessEnv = process.env,
162-
): void {
163-
if (skipNotifications(environment)) return
164-
165-
let command = 'shopify'
166-
const args = ['notifications', 'list']
167-
// Run the Shopify command the same way as the current execution when it's not the global installation
168-
if (argv[0] && argv[0] !== 'shopify') {
169-
command = argv[0]
170-
const indexValue = currentCommand.split(':')[0] ?? ''
171-
const index = argv.indexOf(indexValue)
172-
if (index > 0) args.unshift(...argv.slice(1, index))
173-
}
174-
// eslint-disable-next-line no-void
175-
void exec(command, args, {background: true, env: {...process.env, SHOPIFY_CLI_NO_ANALYTICS: '1'}})
134+
return response.text() as unknown as string
176135
}
177136

178137
/**

packages/cli-kit/src/public/node/system.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ export interface ExecOptions {
2121
signal?: AbortSignal
2222
// Custom handler if process exits with a non-zero code
2323
externalErrorHandler?: (error: unknown) => Promise<void>
24-
background?: boolean
2524
}
2625

2726
/**
@@ -56,11 +55,6 @@ export async function captureOutput(command: string, args: string[], options?: E
5655
*/
5756
export async function exec(command: string, args: string[], options?: ExecOptions): Promise<void> {
5857
const commandProcess = buildExec(command, args, options)
59-
60-
if (options?.background) {
61-
commandProcess.unref()
62-
}
63-
6458
if (options?.stderr && options.stderr !== 'inherit') {
6559
commandProcess.stderr?.pipe(options.stderr, {end: false})
6660
}
@@ -112,18 +106,16 @@ function buildExec(command: string, args: string[], options?: ExecOptions): Exec
112106
env,
113107
cwd: executionCwd,
114108
input: options?.input,
115-
stdio: options?.background ? 'ignore' : options?.stdio,
109+
stdio: options?.stdio,
116110
stdin: options?.stdin,
117111
stdout: options?.stdout === 'inherit' ? 'inherit' : undefined,
118112
stderr: options?.stderr === 'inherit' ? 'inherit' : undefined,
119113
// Setting this to false makes it possible to kill the main process
120114
// and all its sub-processes with Ctrl+C on Windows
121115
windowsHide: false,
122-
detached: options?.background,
123-
cleanup: !options?.background,
124116
})
125117
outputDebug(`
126-
Running system process${options?.background ? ' in background' : ''}:
118+
Running system process:
127119
· Command: ${command} ${args.join(' ')}
128120
· Working directory: ${executionCwd}
129121
`)

packages/cli/src/cli/services/commands/notifications.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
Notification,
77
stringifyFilters,
88
getNotifications,
9-
fetchNotifications,
109
} from '@shopify/cli-kit/node/notifications-system'
1110
import {outputInfo} from '@shopify/cli-kit/node/output'
1211
import {renderSelectPrompt, renderTextPrompt, renderSuccess, renderTable, TableColumn} from '@shopify/cli-kit/node/ui'
@@ -96,7 +95,7 @@ export async function generate() {
9695
}
9796

9897
export async function list() {
99-
const notifications = await fetchNotifications()
98+
const notifications: Notifications = await getNotifications()
10099

101100
const columns: TableColumn<{type: string; title: string; message: string; filters: string}> = {
102101
type: {header: 'Type', color: 'dim'},
@@ -108,7 +107,7 @@ export async function list() {
108107
const rows = notifications.notifications.map((notification: Notification) => {
109108
return {
110109
type: notification.type,
111-
title: notification.title ?? '',
110+
title: notification.title || '',
112111
message: notification.message,
113112
filters: stringifyFilters(notification),
114113
}

0 commit comments

Comments
 (0)