Notification system
WHY are these changes introduced?
Fixes: https://github.com/Shopify/develop-app-inner-loop/issues/1962
We want to notify CLI users about deprecations, new features or events
WHAT is this pull request doing?
Adds three hidden commands:
notifications generate: shows several prompts asking for the notification content, type and filters, and adds it to a local notifications.json file.notifications list: shows the current notifications from the local notifications.jsoncache clear: clears the CLI cache (Mac stores it in~/Library/Preferences/shopify-cli-kit-nodejs/config.json), including notifications stuff and other API responses that are temporarily memorized
Then, before each command we check the remote notifications.json from this repository and show the required ones. The file is cached for 24h.
Demo: https://hackdays.shopify.io/projects/18981
How to test your changes?
Install the snapshot from this branch: npm i -g @shopify/[email protected]
I've uploaded a sample notifications.json to simplify testing:
SHOPIFY_CLI_NOTIFICATIONS_URL=https://shopify.link/MNbn shopify versionSHOPIFY_CLI_NOTIFICATIONS_URL=https://shopify.link/MNbn shopify theme list
When SHOPIFY_CLI_NOTIFICATIONS_URL is not passed, it looks for the file in the main branch of this repository (it doesn't exist yet).
To generate and list notifications:
shopify notifications generateshopify notifications list
To clear the cache, so that it downloads again the configuration file and forgets if a notification was shown:
shopify cache clear
Post-release steps
Merge the internal doc: https://github.com/Shopify/vault-pages/pull/7111 Merge the caution tape bot PR: https://github.com/Shopify/services-db/pull/25841
Measuring impact
How do we know this change was effective? Please choose one:
- [x] n/a - this doesn't need measurement, e.g. a linting rule or a bug-fix
- [ ] Existing analytics will cater for this addition
- [ ] PR includes analytics changes to measure impact
Checklist
- [x] I've considered possible cross-platform impacts (Mac, Linux, Windows)
- [x] I've considered possible documentation changes
Thanks for your contribution!
Depending on what you are working on, you may want to request a review from a Shopify team:
- Themes: @shopify/advanced-edits
- UI extensions: @shopify/ui-extensions-cli
- Checkout UI extensions: @shopify/checkout-ui-extensions-api-stewardship
- Hydrogen: @shopify/hydrogen
- Other: @shopify/app-management
Coverage report
St.:grey_question: |
Category | Percentage | Covered / Total |
|---|---|---|---|
| ๐ก | Statements | 71.92% (-0.13% ๐ป) |
8440/11735 |
| ๐ก | Branches | 68.34% (-0.15% ๐ป) |
4088/5982 |
| ๐ก | Functions | 71.47% (+0.13% ๐ผ) |
2219/3105 |
| ๐ก | Lines | 72.33% (-0.1% ๐ป) |
7978/11030 |
Show new covered files ๐ฃ
St.:grey_question: |
File | Statements | Branches | Functions | Lines |
|---|---|---|---|---|---|
| ๐ด | ... / clear.ts |
0% | 100% | 0% | 0% |
| ๐ด | ... / generate.ts |
0% | 100% | 0% | 0% |
| ๐ด | ... / list.ts |
0% | 100% | 0% | 0% |
| ๐ด | ... / notifications.ts |
0% | 0% | 0% | 0% |
| ๐ข | ... / global-context.ts |
100% | 100% | 100% | 100% |
| ๐ก | ... / notifications-system.ts |
69.89% | 63.49% | 89.47% | 77.92% |
Show files with reduced coverage ๐ป
St.:grey_question: |
File | Statements | Branches | Functions | Lines |
|---|---|---|---|---|---|
| ๐ข | ... / conf-store.ts |
100% | 90.48% (-2.21% ๐ป) |
100% | 100% |
| ๐ข | ... / ConcurrentOutput.tsx |
98.39% (-1.61% ๐ป) |
90.91% (-4.55% ๐ป) |
100% | 98.33% (-1.67% ๐ป) |
| ๐ข | ... / base-command.ts |
84.81% (+0.39% ๐ผ) |
80% (-1.25% ๐ป) |
77.27% | 85.51% (+0.43% ๐ผ) |
| ๐ด | ... / cli.ts |
1.39% (-0.02% ๐ป) |
0% | 0% | 1.54% (-0.02% ๐ป) |
Test suite run success
1908 tests passing in 870 suites.
Report generated by ๐งชjest coverage report action from f0942ec887e705d30f9e8acd0a6d76a9211b6ed0
We detected some changes at either packages/*/src or packages/cli-kit/assets/cli-ruby/** and there are no updates in the .changeset. If the changes are user-facing, run "pnpm changeset add" to track your changes and include them in the next release CHANGELOG.
/snapit
Could we make this easier to test by making the notification system URL overridable by an environment variable? And maybe a command to clear the notification caches?
Looks like there is a formatting issue when the message contains \n, we should either recognize or strip them.
โญโ warning โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ โ
โ Deprecated Shopify CLI version โ
โ โ
December 13, 2024. Upgrade to Shopify CLI 3.67 or later. โI version on
โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
/snapit
๐ซฐโจ Thanks @gonzaloriestra! Your snapshot has been published to npm.
Test the snapshot by intalling your package globally:
pnpm i -g @shopify/[email protected]
After installing, validate the version by running just
shopifyin your terminal If the versions don't match, you might have multiple global instances installed. Usewhich shopifyto find out which one you are running and uninstall it.
This PR seems inactive. If it's still relevant, please add a comment saying so. Otherwise, take no action. โ If there's no activity within a week, then a bot will automatically close this. Thanks for helping to improve Shopify's dev tooling and experience.
We detected some changes at packages/*/src and there are no updates in the .changeset. If the changes are user-facing, run "pnpm changeset add" to track your changes and include them in the next release CHANGELOG.
/snapit
๐ซฐโจ Thanks @gonzaloriestra! Your snapshot has been published to npm.
Test the snapshot by intalling your package globally:
pnpm i -g @shopify/[email protected]
After installing, validate the version by running just
shopifyin your terminal If the versions don't match, you might have multiple global instances installed. Usewhich shopifyto find out which one you are running and uninstall it.
Differences in type declarations
We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:
- Some seemingly private modules might be re-exported through public modules.
- If the branch is behind
mainyou might see odd diffs, rebasemaininto this branch.
New type declarations
packages/cli-kit/dist/public/node/global-context.d.ts
export interface GlobalContext {
currentCommandId: string;
}
/**
* Get the current command ID.
*
* @returns Current command ID.
*/
export declare function getCurrentCommandId(): string;
/**
* Set the current command ID.
*
* @param commandId - Command ID.
*/
export declare function setCurrentCommandId(commandId: string): void;
packages/cli-kit/dist/public/node/notifications-system.d.ts
import { zod } from './schema.js';
declare const NotificationSchema: zod.ZodObject<{
id: zod.ZodString;
message: zod.ZodString;
type: zod.ZodEnum<["info", "warning", "error"]>;
frequency: zod.ZodEnum<["always", "once", "once_a_day", "once_a_week"]>;
ownerChannel: zod.ZodString;
cta: zod.ZodOptional<zod.ZodObject<{
label: zod.ZodString;
url: zod.ZodString;
}, "strip", zod.ZodTypeAny, {
url: string;
label: string;
}, {
url: string;
label: string;
}>>;
title: zod.ZodOptional<zod.ZodString>;
minVersion: zod.ZodOptional<zod.ZodString>;
maxVersion: zod.ZodOptional<zod.ZodString>;
minDate: zod.ZodOptional<zod.ZodString>;
maxDate: zod.ZodOptional<zod.ZodString>;
commands: zod.ZodOptional<zod.ZodArray<zod.ZodString, "many">>;
surface: zod.ZodOptional<zod.ZodString>;
}, "strip", zod.ZodTypeAny, {
id: string;
type: "error" | "info" | "warning";
message: string;
frequency: "always" | "once" | "once_a_day" | "once_a_week";
ownerChannel: string;
cta?: {
url: string;
label: string;
} | undefined;
title?: string | undefined;
minVersion?: string | undefined;
maxVersion?: string | undefined;
minDate?: string | undefined;
maxDate?: string | undefined;
commands?: string[] | undefined;
surface?: string | undefined;
}, {
id: string;
type: "error" | "info" | "warning";
message: string;
frequency: "always" | "once" | "once_a_day" | "once_a_week";
ownerChannel: string;
cta?: {
url: string;
label: string;
} | undefined;
title?: string | undefined;
minVersion?: string | undefined;
maxVersion?: string | undefined;
minDate?: string | undefined;
maxDate?: string | undefined;
commands?: string[] | undefined;
surface?: string | undefined;
}>;
export type Notification = zod.infer<typeof NotificationSchema>;
declare const NotificationsSchema: zod.ZodObject<{
notifications: zod.ZodArray<zod.ZodObject<{
id: zod.ZodString;
message: zod.ZodString;
type: zod.ZodEnum<["info", "warning", "error"]>;
frequency: zod.ZodEnum<["always", "once", "once_a_day", "once_a_week"]>;
ownerChannel: zod.ZodString;
cta: zod.ZodOptional<zod.ZodObject<{
label: zod.ZodString;
url: zod.ZodString;
}, "strip", zod.ZodTypeAny, {
url: string;
label: string;
}, {
url: string;
label: string;
}>>;
title: zod.ZodOptional<zod.ZodString>;
minVersion: zod.ZodOptional<zod.ZodString>;
maxVersion: zod.ZodOptional<zod.ZodString>;
minDate: zod.ZodOptional<zod.ZodString>;
maxDate: zod.ZodOptional<zod.ZodString>;
commands: zod.ZodOptional<zod.ZodArray<zod.ZodString, "many">>;
surface: zod.ZodOptional<zod.ZodString>;
}, "strip", zod.ZodTypeAny, {
id: string;
type: "error" | "info" | "warning";
message: string;
frequency: "always" | "once" | "once_a_day" | "once_a_week";
ownerChannel: string;
cta?: {
url: string;
label: string;
} | undefined;
title?: string | undefined;
minVersion?: string | undefined;
maxVersion?: string | undefined;
minDate?: string | undefined;
maxDate?: string | undefined;
commands?: string[] | undefined;
surface?: string | undefined;
}, {
id: string;
type: "error" | "info" | "warning";
message: string;
frequency: "always" | "once" | "once_a_day" | "once_a_week";
ownerChannel: string;
cta?: {
url: string;
label: string;
} | undefined;
title?: string | undefined;
minVersion?: string | undefined;
maxVersion?: string | undefined;
minDate?: string | undefined;
maxDate?: string | undefined;
commands?: string[] | undefined;
surface?: string | undefined;
}>, "many">;
}, "strip", zod.ZodTypeAny, {
notifications: {
id: string;
type: "error" | "info" | "warning";
message: string;
frequency: "always" | "once" | "once_a_day" | "once_a_week";
ownerChannel: string;
cta?: {
url: string;
label: string;
} | undefined;
title?: string | undefined;
minVersion?: string | undefined;
maxVersion?: string | undefined;
minDate?: string | undefined;
maxDate?: string | undefined;
commands?: string[] | undefined;
surface?: string | undefined;
}[];
}, {
notifications: {
id: string;
type: "error" | "info" | "warning";
message: string;
frequency: "always" | "once" | "once_a_day" | "once_a_week";
ownerChannel: string;
cta?: {
url: string;
label: string;
} | undefined;
title?: string | undefined;
minVersion?: string | undefined;
maxVersion?: string | undefined;
minDate?: string | undefined;
maxDate?: string | undefined;
commands?: string[] | undefined;
surface?: string | undefined;
}[];
}>;
export type Notifications = zod.infer<typeof NotificationsSchema>;
/**
* Shows notifications to the user if they meet the criteria specified in the notifications.json file.
*
* @param currentSurfaces - The surfaces present in the current project (usually for app extensions).
* @returns - A promise that resolves when the notifications have been shown.
*/
export declare function showNotificationsIfNeeded(currentSurfaces?: string[]): Promise<void>;
/**
* Get notifications list from cache (refreshed every hour) or fetch it if not present.
*
* @returns A Notifications object.
*/
export declare function getNotifications(): Promise<Notifications>;
/**
* Filters notifications based on the version of the CLI.
*
* @param notifications - The notifications to filter.
* @param commandId - The command ID to filter by.
* @param currentSurfaces - The surfaces present in the current project (usually for app extensions).
* @param today - The current date.
* @param currentVersion - The current version of the CLI.
* @returns - The filtered notifications.
*/
export declare function filterNotifications(notifications: Notification[], commandId: string, currentSurfaces?: string[], today?: Date, currentVersion?: string): Notification[];
/**
* Returns a string with the filters from a notification, one by line.
*
* @param notification - The notification to get the filters from.
* @returns A string with human-readable filters from the notification.
*/
export declare function stringifyFilters(notification: Notification): string;
/**
* Reads the notifications from the local file.
*
* @returns A Notifications object.
*/
export declare function getLocalNotifications(): Promise<Notifications>;
export {};
Existing type declarations
packages/cli-kit/dist/private/node/conf-store.d.ts
@@ -5,13 +5,17 @@ interface CacheValue<T> {
}
export type IntrospectionUrlKey = ;
export type PackageVersionKey = ;
+export type NotificationsKey = ;
+export type NotificationKey = ;
type MostRecentOccurrenceKey = ;
type RateLimitKey = ;
-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[]>;
}
export interface ConfSchema {
@@ -45,12 +49,13 @@ type CacheValueForKey<TKey extends keyof Cache> = NonNullable<Cache[TKey]>['valu
* @returns The value from the cache or the result of the function.
*/
export declare function cacheRetrieveOrRepopulate(key: ExportedKey, fn: () => Promise<CacheValueForKey<typeof key>>, timeout?: number, config?: LocalStorage<ConfSchema>): Promise<CacheValueForKey<typeof key>>;
+export declare function cacheStore(key: ExportedKey, value: string, config?: LocalStorage<ConfSchema>): void;
/**
* 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 declare function cacheRetrieve(key: ExportedKey, config?: LocalStorage<ConfSchema>): CacheValueForKey<typeof key> | undefined;
+export declare function cacheRetrieve(key: ExportedKey, config?: LocalStorage<ConfSchema>): CacheValue<string> | undefined;
export declare function cacheClear(config?: LocalStorage<ConfSchema>): void;
interface TimeInterval {
days?: number;
packages/cli-kit/dist/public/node/cli.d.ts
@@ -34,4 +34,8 @@ export declare const globalFlags: {
'no-color': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
verbose: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
};
+/**
+ * Clear the CLI cache, used to store some API responses and handle notifications status
+ */
+export declare function clearCache(): void;
export {};
\ No newline at end of file