Skip to content

Commit

Permalink
Merge pull request #4823 from Shopify/notifications-cdn
Browse files Browse the repository at this point in the history
Notification system with CDN
  • Loading branch information
gonzaloriestra authored Nov 11, 2024
2 parents c5e4ec5 + 32f4009 commit bb27257
Show file tree
Hide file tree
Showing 18 changed files with 915 additions and 16 deletions.
7 changes: 7 additions & 0 deletions .changeset/tough-eels-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@shopify/cli-kit': minor
'@shopify/app': minor
'@shopify/cli': minor
---

Notification system
5 changes: 5 additions & 0 deletions packages/app/src/cli/models/app/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {getArrayRejectingUndefined} from '@shopify/cli-kit/common/array'
import {checkIfIgnoredInGitRepository} from '@shopify/cli-kit/node/git'
import {renderInfo} from '@shopify/cli-kit/node/ui'
import {currentProcessIsGlobal} from '@shopify/cli-kit/node/is-global'
import {showNotificationsIfNeeded} from '@shopify/cli-kit/node/notifications-system'
import {globalCLIVersion, localCLIVersion} from '@shopify/cli-kit/node/version'

const defaultExtensionDirectory = 'extensions/*'
Expand Down Expand Up @@ -327,6 +328,10 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
remoteFlags: this.remoteFlags,
})

// Show CLI notifications that are targetted for when your app has specific extension types
const extensionTypes = appClass.realExtensions.map((module) => module.type)
await showNotificationsIfNeeded(extensionTypes)

if (!this.errors.isEmpty()) appClass.errors = this.errors

await logMetadataForLoadedApp(appClass, {
Expand Down
4 changes: 3 additions & 1 deletion packages/cli-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@
"static": [
"@oclif/core",
"./context/utilities.js",
"../../private/node/demo-recorder.js"
"../../private/node/demo-recorder.js",
"../../private/node/conf-store.js",
"url"
]
}
]
Expand Down
7 changes: 4 additions & 3 deletions packages/cli-kit/src/private/node/conf-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,15 @@ describe('cacheRetrieve', () => {
await inTemporaryDirectory(async (cwd) => {
// Given
const config = new LocalStorage<any>({cwd})
const cacheValue = {'identity-introspection-url-IDENTITYURL': {value: 'URL1', timestamp: Date.now()}}
config.set('cache', cacheValue)
const cacheValue = {value: 'URL1', timestamp: Date.now()}
const cacheEntry = {'identity-introspection-url-IDENTITYURL': cacheValue}
config.set('cache', cacheEntry)

// When
const got = cacheRetrieve('identity-introspection-url-IDENTITYURL', config)

// Then
expect(got).toEqual('URL1')
expect(got).toEqual(cacheValue)
})
})

Expand Down
25 changes: 16 additions & 9 deletions packages/cli-kit/src/private/node/conf-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ interface CacheValue<T> {

export type IntrospectionUrlKey = `identity-introspection-url-${string}`
export type PackageVersionKey = `npm-package-${string}`
export type NotificationsKey = `notifications-${string}`
export type NotificationKey = `notification-${string}`
type MostRecentOccurrenceKey = `most-recent-occurrence-${string}`
type RateLimitKey = `rate-limited-occurrences-${string}`

type ExportedKey = IntrospectionUrlKey | PackageVersionKey
type ExportedKey = IntrospectionUrlKey | PackageVersionKey | NotificationsKey | NotificationKey

interface Cache {
[introspectionUrlKey: IntrospectionUrlKey]: CacheValue<string>
[packageVersionKey: PackageVersionKey]: CacheValue<string>
[mostRecentOccurrenceKey: MostRecentOccurrenceKey]: CacheValue<boolean>
[notifications: NotificationsKey]: CacheValue<string>
[notification: NotificationKey]: CacheValue<string>
[MostRecentOccurrenceKey: MostRecentOccurrenceKey]: CacheValue<boolean>
[rateLimitKey: RateLimitKey]: CacheValue<number[]>
}

Expand Down Expand Up @@ -85,28 +89,31 @@ export async function cacheRetrieveOrRepopulate(
timeout?: number,
config = cliKitStore(),
): Promise<CacheValueForKey<typeof key>> {
const cache: Cache = config.get('cache') || {}
const cached = cache[key]
const cached = cacheRetrieve(key, config)

if (cached?.value !== undefined && (timeout === undefined || Date.now() - cached.timestamp < timeout)) {
return cached.value
}

const value = await fn()
cacheStore(key, value, config)
return value
}

export function cacheStore(key: ExportedKey, value: string, config = cliKitStore()): void {
const cache: Cache = config.get('cache') || {}

Check warning on line 104 in packages/cli-kit/src/private/node/conf-store.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/cli-kit/src/private/node/conf-store.ts#L104

[@typescript-eslint/prefer-nullish-coalescing] Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
cache[key] = {value, timestamp: Date.now()}
config.set('cache', cache)
return value
}

/**
* Fetch from cache if already populated, otherwise return undefined.
* @param key - The key to use for the cache.
* @returns The value from the cache or the result of the function.
* @returns The chache element.
*/
export function cacheRetrieve(key: ExportedKey, config = cliKitStore()): CacheValueForKey<typeof key> | undefined {
export function cacheRetrieve(key: ExportedKey, config = cliKitStore()): CacheValue<string> | undefined {
const cache: Cache = config.get('cache') || {}

Check warning on line 115 in packages/cli-kit/src/private/node/conf-store.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/cli-kit/src/private/node/conf-store.ts#L115

[@typescript-eslint/prefer-nullish-coalescing] Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
const cached = cache[key]
return cached?.value
return cache[key]
}

export function cacheClear(config = cliKitStore()): void {
Expand Down
4 changes: 4 additions & 0 deletions packages/cli-kit/src/public/node/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {outputContent, outputInfo, outputToken} from './output.js'
import {terminalSupportsPrompting} from './system.js'
import {hashString} from './crypto.js'
import {isTruthy} from './context/utilities.js'
import {showNotificationsIfNeeded} from './notifications-system.js'
import {setCurrentCommandId} from './global-context.js'
import {JsonMap} from '../../private/common/json.js'
import {underscore} from '../common/string.js'
import {Command, Errors} from '@oclif/core'
Expand Down Expand Up @@ -45,11 +47,13 @@ abstract class BaseCommand extends Command {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected async init(): Promise<any> {
this.exitWithTimestampWhenEnvVariablePresent()
setCurrentCommandId(this.id || '')

Check warning on line 50 in packages/cli-kit/src/public/node/base-command.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/cli-kit/src/public/node/base-command.ts#L50

[@typescript-eslint/prefer-nullish-coalescing] Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
if (!isDevelopment()) {
// This function runs just prior to `run`
await registerCleanBugsnagErrorsFromWithinPlugins(this.config)
}
this.showNpmFlagWarning()
await showNotificationsIfNeeded()
return super.init()
}

Expand Down
9 changes: 8 additions & 1 deletion packages/cli-kit/src/public/node/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {isTruthy} from './context/utilities.js'
import {printEventsJson} from '../../private/node/demo-recorder.js'
import {cacheClear} from '../../private/node/conf-store.js'
import {Flags} from '@oclif/core'
// eslint-disable-next-line @shopify/cli/specific-imports-in-bootstrap-code
import {fileURLToPath} from 'url'

/**
Expand Down Expand Up @@ -202,3 +202,10 @@ export const globalFlags = {
env: 'SHOPIFY_FLAG_VERBOSE',
}),
}

/**
* Clear the CLI cache, used to store some API responses and handle notifications status
*/
export function clearCache(): void {
cacheClear()
}
35 changes: 35 additions & 0 deletions packages/cli-kit/src/public/node/global-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export interface GlobalContext {
currentCommandId: string
}

let _globalContext: GlobalContext | undefined

/**
* Get the global context.
*
* @returns Global context.
*/
function getGlobalContext(): GlobalContext {
if (!_globalContext) {
_globalContext = {currentCommandId: ''}
}
return _globalContext
}

/**
* Get the current command ID.
*
* @returns Current command ID.
*/
export function getCurrentCommandId(): string {
return getGlobalContext().currentCommandId
}

/**
* Set the current command ID.
*
* @param commandId - Command ID.
*/
export function setCurrentCommandId(commandId: string): void {
getGlobalContext().currentCommandId = commandId
}
2 changes: 1 addition & 1 deletion packages/cli-kit/src/public/node/node-package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ export async function checkForNewVersion(
*/
export function checkForCachedNewVersion(dependency: string, currentVersion: string): string | undefined {
const cacheKey: PackageVersionKey = `npm-package-${dependency}`
const lastVersion = cacheRetrieve(cacheKey)
const lastVersion = cacheRetrieve(cacheKey)?.value

if (lastVersion && new SemVer(currentVersion).compare(lastVersion) < 0) {
return lastVersion
Expand Down
Loading

0 comments on commit bb27257

Please sign in to comment.