From 45d64246839f8abdee5aecf6496979b6d7a78cf9 Mon Sep 17 00:00:00 2001 From: Josh Faigan Date: Fri, 1 Nov 2024 10:48:06 -0400 Subject: [PATCH] Add shortcut keys to theme dev This commit adds shortcut keys to the theme dev CLI service. The keys are as follows: e - open theme editor t - preview your theme locally p - preview your theme (share) g - preview gift cards --- .changeset/swift-frogs-fail.md | 5 + packages/theme/src/cli/services/dev.test.ts | 118 ++++++++++++++------ packages/theme/src/cli/services/dev.ts | 83 +++++++++++--- 3 files changed, 155 insertions(+), 51 deletions(-) create mode 100644 .changeset/swift-frogs-fail.md diff --git a/.changeset/swift-frogs-fail.md b/.changeset/swift-frogs-fail.md new file mode 100644 index 0000000000..9ab30173dc --- /dev/null +++ b/.changeset/swift-frogs-fail.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme': minor +--- + +Add shortcut keys to theme dev commands diff --git a/packages/theme/src/cli/services/dev.test.ts b/packages/theme/src/cli/services/dev.test.ts index 678f9b30cf..63d3ec8efe 100644 --- a/packages/theme/src/cli/services/dev.test.ts +++ b/packages/theme/src/cli/services/dev.test.ts @@ -1,4 +1,4 @@ -import {dev, DevOptions, renderLinks} from './dev.js' +import {dev, DevOptions, openURLSafely, renderLinks} from './dev.js' import {setupDevServer} from '../utilities/theme-environment/theme-environment.js' import {mountThemeFileSystem} from '../utilities/theme-fs.js' import {fakeThemeFileSystem} from '../utilities/theme-fs/theme-fs-mock-factory.js' @@ -11,7 +11,8 @@ import {describe, expect, test, vi} from 'vitest' import {buildTheme} from '@shopify/cli-kit/node/themes/factories' import {DEVELOPMENT_THEME_ROLE} from '@shopify/cli-kit/node/themes/utils' import {fetchChecksums} from '@shopify/cli-kit/node/themes/api' -import {renderSuccess} from '@shopify/cli-kit/node/ui' +import {renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui' +import {openURL} from '@shopify/cli-kit/node/system' vi.mock('@shopify/cli-kit/node/ui') vi.mock('@shopify/cli-kit/node/themes/api') @@ -21,6 +22,16 @@ vi.mock('../utilities/theme-environment/storefront-session.js') vi.mock('../utilities/theme-environment/theme-environment.js') vi.mock('../utilities/theme-fs-empty.js') vi.mock('../utilities/theme-fs.js') +vi.mock('@shopify/cli-kit/node/colors', () => ({ + default: { + bold: (str: string) => str, + cyan: (str: string) => str, + gray: (str: string) => str, + }, +})) +vi.mock('@shopify/cli-kit/node/system', () => ({ + openURL: vi.fn(), +})) describe('dev', () => { const store = 'my-store.myshopify.com' @@ -95,50 +106,83 @@ describe('dev', () => { }) }) - describe('renderLinks', async () => { - test('renders "dev" command links', async () => { - // Given - const themeId = theme.id.toString() + test('renders "dev" command links', async () => { + // Given + const themeId = theme.id.toString() + const host = '127.0.0.1' + const port = '9292' + const urls = { + local: `http://${host}:${port}`, + giftCard: `http://${host}:${port}/gift_cards/[store_id]/preview`, + themeEditor: `https://${store}/admin/themes/${themeId}/editor`, + preview: `https://${store}/?preview_theme_id=${themeId}`, + } - // When - renderLinks(store, themeId) + // When + renderLinks(urls) - // Then - expect(renderSuccess).toHaveBeenCalledWith({ - body: [ - { - list: { - items: ['http://127.0.0.1:9292'], - title: { - bold: 'Preview your theme', + // Then + expect(renderSuccess).toHaveBeenCalledWith({ + body: [ + { + list: { + title: 'Preview your theme (t)', + items: [ + { + link: { + url: 'http://127.0.0.1:9292', + }, }, + ], + }, + }, + ], + nextSteps: [ + [ + { + link: { + label: `Share your theme preview (p)`, + url: `https://${store}/?preview_theme_id=${themeId}`, }, }, + { + subdued: `https://${store}/?preview_theme_id=${themeId}`, + }, ], - nextSteps: [ - [ - { - link: { - label: 'Preview your gift cards', - url: 'http://127.0.0.1:9292/gift_cards/[store_id]/preview', - }, - }, - ], - [ - { - link: { - label: 'Customize your theme at the theme editor', - url: 'https://my-store.myshopify.com/admin/themes/123/editor', - }, + [ + { + link: { + label: `Customize your theme at the theme editor (e)`, + url: `https://${store}/admin/themes/${themeId}/editor`, }, - ], - [ - 'Share your theme preview', - { - subdued: '\nhttps://my-store.myshopify.com/?preview_theme_id=123', + }, + ], + [ + { + link: { + label: 'Preview your gift cards (g)', + url: 'http://127.0.0.1:9292/gift_cards/[store_id]/preview', }, - ], + }, ], + ], + }) + }) + describe('openURLSafely', () => { + test('calls renderWarning when openURL fails', async () => { + // Given + const error = new Error('Failed to open URL') + vi.mocked(openURL).mockRejectedValueOnce(error) + + // When + openURLSafely('http://127.0.0.1:9292', 'localhost') + + // Then + await vi.waitFor(() => { + expect(renderWarning).toHaveBeenCalledWith({ + headline: 'Failed to open localhost.', + body: error.stack ?? error.message, + }) }) }) }) diff --git a/packages/theme/src/cli/services/dev.ts b/packages/theme/src/cli/services/dev.ts index cdc3a9e987..9223b4633d 100644 --- a/packages/theme/src/cli/services/dev.ts +++ b/packages/theme/src/cli/services/dev.ts @@ -12,6 +12,8 @@ import {Theme} from '@shopify/cli-kit/node/themes/types' import {checkPortAvailability, getAvailableTCPPort} from '@shopify/cli-kit/node/tcp' import {AbortError} from '@shopify/cli-kit/node/error' import {openURL} from '@shopify/cli-kit/node/system' +import chalk from '@shopify/cli-kit/node/colors' +import readline from 'readline' const DEFAULT_HOST = '127.0.0.1' const DEFAULT_PORT = '9292' @@ -61,6 +63,13 @@ export async function dev(options: DevOptions) { const port = options.port ?? String(await getAvailableTCPPort(Number(DEFAULT_PORT))) + const urls = { + local: `http://${host}:${port}`, + giftCard: `http://${host}:${port}/gift_cards/[store_id]/preview`, + themeEditor: `https://${options.store}/admin/themes/${options.theme.id}/editor`, + preview: `https://${options.store}/?preview_theme_id=${options.theme.id}`, + } + const storefrontPassword = await storefrontPasswordPromise const session = await initializeDevServerSession( options.theme.id.toString(), @@ -98,23 +107,64 @@ export async function dev(options: DevOptions) { await renderDevSetupProgress() await serverStart() - renderLinks(options.store, String(options.theme.id), host, port) + renderLinks(urls) if (options.open) { - openURL(`http://${host}:${port}`).catch((error: Error) => { - renderWarning({headline: 'Failed to open the development server.', body: error.stack ?? error.message}) + openURLSafely(urls.local, 'development server') + } + + readline.emitKeypressEvents(process.stdin) + if (process.stdin.isTTY) { + process.stdin.setRawMode(true) + } + + process.stdin.on('keypress', (_str, key) => { + if (key.ctrl && key.name === 'c') { + process.exit() + } + + switch (key.name) { + case 't': + openURLSafely(urls.local, 'localhost') + break + case 'p': + openURLSafely(urls.preview, 'theme preview') + break + case 'e': + openURLSafely(urls.themeEditor, 'theme editor') + break + case 'g': + openURLSafely(urls.giftCard, 'gift card preview') + break + } + }) +} + +export function openURLSafely(url: string, label: string) { + openURL(url).catch(handleOpenURLError(label)) +} + +function handleOpenURLError(message: string) { + return (error: Error) => { + renderWarning({ + headline: `Failed to open ${message}.`, + body: error.stack ?? error.message, }) } } -export function renderLinks(store: string, themeId: string, host = DEFAULT_HOST, port = DEFAULT_PORT) { - const remoteUrl = `https://${store}` - const localUrl = `http://${host}:${port}` +export function renderLinks(urls: {local: string; giftCard: string; themeEditor: string; preview: string}) { renderSuccess({ body: [ { list: { - title: {bold: 'Preview your theme'}, - items: [localUrl], + title: chalk.bold('Preview your theme ') + chalk.cyan('(t)'), + items: [ + { + link: { + url: urls.local, + }, + }, + ], }, }, ], @@ -122,23 +172,28 @@ export function renderLinks(store: string, themeId: string, host = DEFAULT_HOST, [ { link: { - label: 'Preview your gift cards', - url: `${localUrl}/gift_cards/[store_id]/preview`, + label: `Share your theme preview ${chalk.cyan('(p)')}`, + url: urls.preview, }, }, + { + subdued: urls.preview, + }, ], [ { link: { - label: 'Customize your theme at the theme editor', - url: `${remoteUrl}/admin/themes/${themeId}/editor`, + label: `Customize your theme at the theme editor ${chalk.cyan('(e)')}`, + url: urls.themeEditor, }, }, ], [ - 'Share your theme preview', { - subdued: `\n${remoteUrl}/?preview_theme_id=${themeId}`, + link: { + label: `Preview your gift cards ${chalk.cyan('(g)')}`, + url: urls.giftCard, + }, }, ], ],