From f6a5e0abf0b4edc4c3772f9bde0e0a04335f1ec3 Mon Sep 17 00:00:00 2001 From: James Meng Date: Mon, 23 Sep 2024 13:36:12 -0700 Subject: [PATCH 1/8] Move logic from command layer to service layer --- packages/theme/src/cli/commands/theme/push.ts | 116 +++------- packages/theme/src/cli/services/push.ts | 200 +++++++++++++++++- 2 files changed, 222 insertions(+), 94 deletions(-) diff --git a/packages/theme/src/cli/commands/theme/push.ts b/packages/theme/src/cli/commands/theme/push.ts index 7bd34ce9cd2..577dd14dc77 100644 --- a/packages/theme/src/cli/commands/theme/push.ts +++ b/packages/theme/src/cli/commands/theme/push.ts @@ -2,21 +2,16 @@ import {themeFlags} from '../../flags.js' import {ensureThemeStore} from '../../utilities/theme-store.js' import ThemeCommand, {FlagValues} from '../../utilities/theme-command.js' import {DevelopmentThemeManager} from '../../utilities/development-theme-manager.js' -import {findOrSelectTheme} from '../../utilities/theme-selector.js' -import {showEmbeddedCLIWarning} from '../../utilities/embedded-cli-warning.js' -import {push} from '../../services/push.js' +import {push, PushFlags} from '../../services/push.js' import {hasRequiredThemeDirectories} from '../../utilities/theme-fs.js' import {currentDirectoryConfirmed} from '../../utilities/theme-ui.js' +import {showEmbeddedCLIWarning} from '../../utilities/embedded-cli-warning.js' import {Flags} from '@oclif/core' import {globalFlags} from '@shopify/cli-kit/node/cli' -import {execCLI2} from '@shopify/cli-kit/node/ruby' -import {AdminSession, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' -import {useEmbeddedThemeCLI} from '@shopify/cli-kit/node/context/local' -import {RenderConfirmationPromptOptions, renderConfirmationPrompt} from '@shopify/cli-kit/node/ui' -import {LIVE_THEME_ROLE, Role, UNPUBLISHED_THEME_ROLE, promptThemeName} from '@shopify/cli-kit/node/themes/utils' +import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' import {cwd, resolvePath} from '@shopify/cli-kit/node/path' -import {Theme} from '@shopify/cli-kit/node/themes/types' -import {createTheme} from '@shopify/cli-kit/node/themes/api' +import {useEmbeddedThemeCLI} from '@shopify/cli-kit/node/context/local' +import {execCLI2} from '@shopify/cli-kit/node/ruby' export default class Push extends ThemeCommand { static summary = 'Uploads your local theme files to the connected store, overwriting the remote version if specified.' @@ -139,33 +134,36 @@ export default class Push extends ThemeCommand { async run(): Promise { const {flags} = await this.parse(Push) - const {path, force} = flags - const store = ensureThemeStore(flags) - const adminSession = await ensureAuthenticatedThemes(store, flags.password) - const workingDirectory = path ? resolvePath(path) : cwd() - if (!(await hasRequiredThemeDirectories(workingDirectory)) && !(await currentDirectoryConfirmed(force))) { + if (flags.password || flags.legacy) { + await this.execLegacyPush() return } - if (!flags.legacy && !flags.password) { - const {path, nodelete, publish, json, force, ignore, only} = flags + const pushFlags: PushFlags = { + path: flags.path, + password: flags.password, + environment: flags.environment, + store: flags.store, + theme: flags.theme, + development: flags.development, + live: flags.live, + unpublished: flags.unpublished, + } - const selectedTheme: Theme | undefined = await createOrSelectTheme(adminSession, flags) - if (!selectedTheme) { - return - } + await push(pushFlags) + } + + async execLegacyPush() { + const {flags} = await this.parse(Push) + const path = flags.path || cwd() + const force = flags.force || false - await push(selectedTheme, adminSession, { - path, - nodelete, - publish, - json, - force, - ignore, - only, - }) + const store = ensureThemeStore({store: flags.store}) + const adminSession = await ensureAuthenticatedThemes(store, flags.password) + const workingDirectory = path ? resolvePath(path) : cwd() + if (!(await hasRequiredThemeDirectories(workingDirectory)) && !(await currentDirectoryConfirmed(force))) { return } @@ -197,61 +195,3 @@ export default class Push extends ThemeCommand { await execCLI2(command, {store, adminToken: adminSession.token}) } } - -export interface ThemeSelectionOptions { - live?: boolean - development?: boolean - unpublished?: boolean - theme?: string - 'allow-live'?: boolean -} - -export async function createOrSelectTheme( - adminSession: AdminSession, - flags: ThemeSelectionOptions, -): Promise { - const {live, development, unpublished, theme} = flags - - if (development) { - const themeManager = new DevelopmentThemeManager(adminSession) - return themeManager.findOrCreate() - } else if (unpublished) { - const themeName = theme || (await promptThemeName('Name of the new theme')) - return createTheme( - { - name: themeName, - role: UNPUBLISHED_THEME_ROLE, - }, - adminSession, - ) - } else { - const selectedTheme = await findOrSelectTheme(adminSession, { - header: 'Select a theme to push to:', - filter: { - live, - theme, - }, - }) - - if (await confirmPushToTheme(selectedTheme.role as Role, flags['allow-live'], adminSession.storeFqdn)) { - return selectedTheme - } - } -} - -async function confirmPushToTheme(themeRole: Role, allowLive: boolean | undefined, storeFqdn: string) { - if (themeRole === LIVE_THEME_ROLE) { - if (allowLive) { - return true - } - - const options: RenderConfirmationPromptOptions = { - message: `Push theme files to the ${themeRole} theme on ${storeFqdn}?`, - confirmationMessage: 'Yes, confirm changes', - cancellationMessage: 'Cancel', - } - - return renderConfirmationPrompt(options) - } - return true -} diff --git a/packages/theme/src/cli/services/push.ts b/packages/theme/src/cli/services/push.ts index f9a0f54a53f..3bb49c6b6d0 100644 --- a/packages/theme/src/cli/services/push.ts +++ b/packages/theme/src/cli/services/push.ts @@ -1,12 +1,32 @@ -import {mountThemeFileSystem} from '../utilities/theme-fs.js' +/* eslint-disable tsdoc/syntax */ +import {hasRequiredThemeDirectories, mountThemeFileSystem} from '../utilities/theme-fs.js' import {uploadTheme} from '../utilities/theme-uploader.js' -import {themeComponent} from '../utilities/theme-ui.js' -import {AdminSession} from '@shopify/cli-kit/node/session' -import {fetchChecksums, publishTheme} from '@shopify/cli-kit/node/themes/api' +import {currentDirectoryConfirmed, themeComponent} from '../utilities/theme-ui.js' +import {ensureThemeStore} from '../utilities/theme-store.js' +import {DevelopmentThemeManager} from '../utilities/development-theme-manager.js' +import {findOrSelectTheme} from '../utilities/theme-selector.js' +import {Role} from '../utilities/theme-selector/fetch.js' +import {AdminSession, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' +import {createTheme, fetchChecksums, publishTheme} from '@shopify/cli-kit/node/themes/api' import {Result, Theme} from '@shopify/cli-kit/node/themes/types' import {outputInfo} from '@shopify/cli-kit/node/output' -import {renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui' +import { + renderConfirmationPrompt, + RenderConfirmationPromptOptions, + renderSuccess, + renderWarning, +} from '@shopify/cli-kit/node/ui' import {themeEditorUrl, themePreviewUrl} from '@shopify/cli-kit/node/themes/urls' +import {cwd, resolvePath} from '@shopify/cli-kit/node/path' +import {LIVE_THEME_ROLE, promptThemeName, UNPUBLISHED_THEME_ROLE} from '@shopify/cli-kit/node/themes/utils' + +interface ThemeSelectionOptions { + live?: boolean + development?: boolean + unpublished?: boolean + theme?: string + 'allow-live'?: boolean +} interface PushOptions { path: string @@ -30,7 +50,87 @@ interface JsonOutput { } } -export async function push(theme: Theme, session: AdminSession, options: PushOptions) { +export interface PushFlags { + path?: string + password?: string + store?: string + environment?: string + theme?: string + development?: boolean + live?: boolean + unpublished?: boolean + nodelete?: boolean + only?: string[] + ignore?: string[] + json?: boolean + allowLive?: boolean + publish?: boolean + force?: boolean + noColor?: boolean + verbose?: boolean +} + +/** + * Initiates the push process based on provided flags. + * + * @param {PushFlags} flags - The flags for the push operation. + * @param {string} [flags.path] - The path to your theme directory. + * @param {string} [flags.password] - Password generated from the Theme Access app. + * @param {string} [flags.store] - Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com). + * @param {string} [flags.environment] - The environment to apply to the current command. + * @param {string} [flags.theme] - Theme ID or name of the remote theme. + * @param {boolean} [flags.development] - Push theme files from your remote development theme. + * @param {boolean} [flags.live] - Push theme files from your remote live theme. + * @param {boolean} [flags.unpublished] - Create a new unpublished theme and push to it. + * @param {boolean} [flags.nodelete] - Runs the push command without deleting local files. + * @param {string[]} [flags.only] - Download only the specified files (Multiple flags allowed). + * @param {string[]} [flags.ignore] - Skip downloading the specified files (Multiple flags allowed). + * @param {boolean} [flags.json] - Output JSON instead of a UI. + * @param {boolean} [flags.allowLive] - Allow push to a live theme. + * @param {boolean} [flags.publish] - Publish as the live theme after uploading. + * @param {boolean} [flags.legacy] - Use the legacy Ruby implementation for the `shopify theme push` command. + * @param {boolean} [flags.force] - Proceed without confirmation, if current directory does not seem to be theme directory. + * @param {boolean} [flags.noColor] - Disable color output. + * @param {boolean} [flags.verbose] - Increase the verbosity of the output. + * @returns {Promise} Resolves when the push operation is complete. + */ +export async function push(flags: PushFlags) { + const {path} = flags + const force = flags.force ?? false + + const store = ensureThemeStore({store: flags.store}) + const adminSession = await ensureAuthenticatedThemes(store, flags.password) + + const workingDirectory = path ? resolvePath(path) : cwd() + if (!(await hasRequiredThemeDirectories(workingDirectory)) && !(await currentDirectoryConfirmed(force))) { + return + } + + const selectedTheme: Theme | undefined = await createOrSelectTheme(adminSession, flags) + if (!selectedTheme) { + return + } + + await executePush(selectedTheme, adminSession, { + path: workingDirectory, + nodelete: flags.nodelete || false, + publish: flags.publish || false, + json: flags.json || false, + force, + ignore: flags.ignore || [], + only: flags.only || [], + }) +} + +/** + * Executes the push operation for a given theme. + * + * @param {Theme} theme - The theme to be pushed. + * @param {AdminSession} session - The admin session for the theme. + * @param {PushOptions} options - The options for the push operation. + * @returns {Promise} Resolves when the push operation is complete. + */ +async function executePush(theme: Theme, session: AdminSession, options: PushOptions) { const themeChecksums = await fetchChecksums(theme.id, session) const themeFileSystem = mountThemeFileSystem(options.path, {filters: options}) @@ -51,6 +151,12 @@ export async function push(theme: Theme, session: AdminSession, options: PushOpt await handlePushOutput(uploadResults, theme, session, options) } +/** + * Checks if there are any upload errors in the results. + * + * @param {Map} results - The map of upload results. + * @returns {boolean} - Returns true if there are any upload errors, otherwise false. + */ function hasUploadErrors(results: Map): boolean { for (const [_key, result] of results.entries()) { if (!result.success) { @@ -60,6 +166,15 @@ function hasUploadErrors(results: Map): boolean { return false } +/** + * Handles the output based on the push operation results. + * + * @param {Map} results - The map of upload results. + * @param {Theme} theme - The theme being pushed. + * @param {AdminSession} session - The admin session for the theme. + * @param {PushOptions} options - The options for the push operation. + * @returns {Promise} Resolves when the output handling is complete. + */ async function handlePushOutput( results: Map, theme: Theme, @@ -77,6 +192,14 @@ async function handlePushOutput( } } +/** + * Handles the JSON output for the push operation. + * + * @param {Theme} theme - The theme being pushed. + * @param {boolean} hasErrors - Indicates if there were any errors during the push operation. + * @param {AdminSession} session - The admin session for the theme. + * @returns {void} + */ function handleJsonOutput(theme: Theme, hasErrors: boolean, session: AdminSession) { const output: JsonOutput = { theme: { @@ -96,6 +219,13 @@ function handleJsonOutput(theme: Theme, hasErrors: boolean, session: AdminSessio outputInfo(JSON.stringify(output)) } +/** + * Handles the output for the publish operation. + * + * @param {boolean} hasErrors - Indicates if there were any errors during the push operation. + * @param {AdminSession} session - The admin session for the theme. + * @returns {void} + */ function handlePublishOutput(hasErrors: boolean, session: AdminSession) { if (hasErrors) { renderWarning({body: `Your theme was published with errors and is now live at https://${session.storeFqdn}`}) @@ -104,6 +234,14 @@ function handlePublishOutput(hasErrors: boolean, session: AdminSession) { } } +/** + * Handles the output for the push operation. + * + * @param {Theme} theme - The theme being pushed. + * @param {boolean} hasErrors - Indicates if there were any errors during the push operation. + * @param {AdminSession} session - The admin session for the theme. + * @returns {void} + */ function handleOutput(theme: Theme, hasErrors: boolean, session: AdminSession) { const nextSteps = [ [ @@ -136,3 +274,53 @@ function handleOutput(theme: Theme, hasErrors: boolean, session: AdminSession) { }) } } + +export async function createOrSelectTheme( + adminSession: AdminSession, + flags: ThemeSelectionOptions, +): Promise { + const {live, development, unpublished, theme} = flags + + if (development) { + const themeManager = new DevelopmentThemeManager(adminSession) + return themeManager.findOrCreate() + } else if (unpublished) { + const themeName = theme || (await promptThemeName('Name of the new theme')) + return createTheme( + { + name: themeName, + role: UNPUBLISHED_THEME_ROLE, + }, + adminSession, + ) + } else { + const selectedTheme = await findOrSelectTheme(adminSession, { + header: 'Select a theme to push to:', + filter: { + live, + theme, + }, + }) + + if (await confirmPushToTheme(selectedTheme.role as Role, flags['allow-live'], adminSession.storeFqdn)) { + return selectedTheme + } + } +} + +async function confirmPushToTheme(themeRole: Role, allowLive: boolean | undefined, storeFqdn: string) { + if (themeRole === LIVE_THEME_ROLE) { + if (allowLive) { + return true + } + + const options: RenderConfirmationPromptOptions = { + message: `Push theme files to the ${themeRole} theme on ${storeFqdn}?`, + confirmationMessage: 'Yes, confirm changes', + cancellationMessage: 'Cancel', + } + + return renderConfirmationPrompt(options) + } + return true +} From 2568b91bf313304545bb8c9c3fbde6086fd425fd Mon Sep 17 00:00:00 2001 From: James Meng Date: Mon, 23 Sep 2024 13:36:19 -0700 Subject: [PATCH 2/8] Update tests --- .../theme/src/cli/commands/theme/push.test.ts | 170 ++---------------- packages/theme/src/cli/services/push.test.ts | 63 +++++-- 2 files changed, 59 insertions(+), 174 deletions(-) diff --git a/packages/theme/src/cli/commands/theme/push.test.ts b/packages/theme/src/cli/commands/theme/push.test.ts index 3380235eaae..e0fa0136152 100644 --- a/packages/theme/src/cli/commands/theme/push.test.ts +++ b/packages/theme/src/cli/commands/theme/push.test.ts @@ -1,17 +1,12 @@ -import Push, {ThemeSelectionOptions, createOrSelectTheme} from './push.js' +import Push from './push.js' import {DevelopmentThemeManager} from '../../utilities/development-theme-manager.js' import {ensureThemeStore} from '../../utilities/theme-store.js' -import {findOrSelectTheme} from '../../utilities/theme-selector.js' -import {push} from '../../services/push.js' import {getDevelopmentTheme, removeDevelopmentTheme, setDevelopmentTheme} from '../../services/local-storage.js' import {describe, vi, expect, test, beforeEach} from 'vitest' import {Config} from '@oclif/core' import {execCLI2} from '@shopify/cli-kit/node/ruby' import {AdminSession, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' import {buildTheme} from '@shopify/cli-kit/node/themes/factories' -import {renderConfirmationPrompt, renderTextPrompt} from '@shopify/cli-kit/node/ui' -import {DEVELOPMENT_THEME_ROLE, LIVE_THEME_ROLE, UNPUBLISHED_THEME_ROLE} from '@shopify/cli-kit/node/themes/utils' -import {createTheme, fetchTheme} from '@shopify/cli-kit/node/themes/api' vi.mock('../../services/push.js') vi.mock('../../utilities/theme-store.js') @@ -34,157 +29,6 @@ describe('Push', () => { vi.mocked(removeDevelopmentTheme).mockImplementation(() => undefined) }) - describe('run with TS implementation', () => { - test('should run the Ruby implementation if the password flag is provided', async () => { - // Given - const theme = buildTheme({id: 1, name: 'Theme', role: 'development'})! - vi.spyOn(DevelopmentThemeManager.prototype, 'fetch').mockResolvedValue(theme) - - // When - await runPushCommand(['--password', '123'], path, adminSession) - - // Then - expectCLI2ToHaveBeenCalledWith(`theme push ${path} --development-theme-id ${theme.id}`) - }) - - test('should pass call the CLI 3 command', async () => { - // Given - const theme = buildTheme({id: 1, name: 'Theme', role: 'development'})! - vi.mocked(findOrSelectTheme).mockResolvedValue(theme) - - // When - await runPushCommand([], path, adminSession) - - // Then - expect(execCLI2).not.toHaveBeenCalled() - expect(push).toHaveBeenCalled() - }) - }) - - describe('createOrSelectTheme', async () => { - test('creates unpublished theme when unpublished flag is provided', async () => { - // Given - vi.mocked(createTheme).mockResolvedValue(buildTheme({id: 2, name: 'Theme', role: UNPUBLISHED_THEME_ROLE})) - vi.mocked(fetchTheme).mockResolvedValue(undefined) - - const flags: ThemeSelectionOptions = {unpublished: true} - - // When - const theme = await createOrSelectTheme(adminSession, flags) - - // Then - expect(theme).toMatchObject({role: UNPUBLISHED_THEME_ROLE}) - expect(setDevelopmentTheme).not.toHaveBeenCalled() - }) - - test('creates development theme when development flag is provided', async () => { - // Given - vi.mocked(createTheme).mockResolvedValue(buildTheme({id: 1, name: 'Theme', role: DEVELOPMENT_THEME_ROLE})) - vi.mocked(fetchTheme).mockResolvedValue(undefined) - const flags: ThemeSelectionOptions = {development: true} - - // When - const theme = await createOrSelectTheme(adminSession, flags) - - // Then - expect(theme).toMatchObject({role: DEVELOPMENT_THEME_ROLE}) - expect(setDevelopmentTheme).toHaveBeenCalled() - }) - - test('creates development theme when development and unpublished flags are provided', async () => { - // Given - vi.mocked(createTheme).mockResolvedValue(buildTheme({id: 1, name: 'Theme', role: DEVELOPMENT_THEME_ROLE})) - vi.mocked(fetchTheme).mockResolvedValue(undefined) - const flags: ThemeSelectionOptions = {development: true, unpublished: true} - - // When - const theme = await createOrSelectTheme(adminSession, flags) - - // Then - expect(theme).toMatchObject({role: DEVELOPMENT_THEME_ROLE}) - }) - - test('returns live theme when live flag is provided', async () => { - // Given - vi.mocked(findOrSelectTheme).mockResolvedValue(buildTheme({id: 3, name: 'Live Theme', role: LIVE_THEME_ROLE})!) - const flags: ThemeSelectionOptions = {live: true, 'allow-live': true} - - // When - const theme = await createOrSelectTheme(adminSession, flags) - - // Then - expect(theme).toMatchObject({role: LIVE_THEME_ROLE}) - }) - - test("renders confirmation prompt if 'allow-live' flag is not provided and selected theme role is live", async () => { - // Given - vi.mocked(findOrSelectTheme).mockResolvedValue(buildTheme({id: 3, name: 'Live Theme', role: LIVE_THEME_ROLE})!) - vi.mocked(renderConfirmationPrompt).mockResolvedValue(true) - const flags: ThemeSelectionOptions = {live: true} - - // When - const theme = await createOrSelectTheme(adminSession, flags) - - // Then - expect(theme?.role).toBe(LIVE_THEME_ROLE) - expect(renderConfirmationPrompt).toHaveBeenCalled() - }) - - test("renders confirmation prompt if 'allow-live' flag is not provided and live theme is specified via theme flag", async () => { - // Given - vi.mocked(findOrSelectTheme).mockResolvedValue(buildTheme({id: 3, name: 'Live Theme', role: LIVE_THEME_ROLE})!) - vi.mocked(renderConfirmationPrompt).mockResolvedValue(true) - const flags: ThemeSelectionOptions = {theme: '3'} - - // When - const theme = await createOrSelectTheme(adminSession, flags) - - // Then - expect(theme?.role).toBe(LIVE_THEME_ROLE) - expect(renderConfirmationPrompt).toHaveBeenCalled() - }) - - test('returns undefined if live theme confirmation prompt is not confirmed', async () => { - // Given - vi.mocked(findOrSelectTheme).mockResolvedValue(buildTheme({id: 3, name: 'Live Theme', role: LIVE_THEME_ROLE})!) - vi.mocked(renderConfirmationPrompt).mockResolvedValue(false) - const flags: ThemeSelectionOptions = {live: true} - - // When - const theme = await createOrSelectTheme(adminSession, flags) - - // Then - expect(theme).toBeUndefined() - }) - - test('returns undefined if confirmation prompt is rejected', async () => { - // Given - vi.mocked(findOrSelectTheme).mockResolvedValue(buildTheme({id: 3, name: 'Live Theme', role: LIVE_THEME_ROLE})!) - vi.mocked(renderConfirmationPrompt).mockResolvedValue(false) - const flags = {live: true} - - // When - const theme = await createOrSelectTheme(adminSession, flags) - - // Then - expect(theme).toBeUndefined() - }) - - test('renders text prompt if unpublished flag is provided and theme flag is not provided', async () => { - // Given - const flags = {unpublished: true} - - // When - await createOrSelectTheme(adminSession, flags) - - // Then - expect(renderTextPrompt).toHaveBeenCalledWith({ - message: 'Name of the new theme', - defaultValue: expect.any(String), - }) - }) - }) - describe('run with Ruby implementation', () => { test('should pass development theme from local storage to Ruby implementation', async () => { // Given @@ -233,6 +77,18 @@ describe('Push', () => { expect(DevelopmentThemeManager.prototype.fetch).not.toHaveBeenCalled() expectCLI2ToHaveBeenCalledWith(`theme push ${path} --theme ${theme.id} --development-theme-id ${theme.id}`) }) + + test('should run the Ruby implementation if the password flag is provided', async () => { + // Given + const theme = buildTheme({id: 1, name: 'Theme', role: 'development'})! + vi.spyOn(DevelopmentThemeManager.prototype, 'fetch').mockResolvedValue(theme) + + // When + await runPushCommand(['--password', '123'], path, adminSession) + + // Then + expectCLI2ToHaveBeenCalledWith(`theme push ${path} --development-theme-id ${theme.id}`) + }) }) async function run(argv: string[]) { diff --git a/packages/theme/src/cli/services/push.test.ts b/packages/theme/src/cli/services/push.test.ts index 2da02640a3e..7344b348c50 100644 --- a/packages/theme/src/cli/services/push.test.ts +++ b/packages/theme/src/cli/services/push.test.ts @@ -1,44 +1,73 @@ -import {push} from './push.js' -import {hasRequiredThemeDirectories} from '../utilities/theme-fs.js' +import {createOrSelectTheme, push} from './push.js' +import {PullFlags} from './pull.js' import {uploadTheme} from '../utilities/theme-uploader.js' -import {resolvePath} from '@shopify/cli-kit/node/path' +import {ensureThemeStore} from '../utilities/theme-store.js' +import {findOrSelectTheme} from '../utilities/theme-selector.js' import {buildTheme} from '@shopify/cli-kit/node/themes/factories' import {test, describe, vi, expect, beforeEach} from 'vitest' -import {fetchChecksums, publishTheme} from '@shopify/cli-kit/node/themes/api' +import {createTheme, fetchTheme, publishTheme} from '@shopify/cli-kit/node/themes/api' +import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' +import {promptThemeName, UNPUBLISHED_THEME_ROLE} from '@shopify/cli-kit/node/themes/utils' vi.mock('../utilities/theme-fs.js') -vi.mock('@shopify/cli-kit/node/path') vi.mock('../utilities/theme-uploader.js') +vi.mock('../utilities/theme-store.js') +vi.mock('../utilities/theme-selector.js') +vi.mock('@shopify/cli-kit/node/themes/utils') +vi.mock('@shopify/cli-kit/node/session') +vi.mock('@shopify/cli-kit/node/path') vi.mock('@shopify/cli-kit/node/themes/api') -describe('push', () => { - const adminSession = {token: '', storeFqdn: ''} +const path = '/my-theme' +const defaultFlags: PullFlags = { + path, + development: false, + live: false, + nodelete: false, + only: [], + ignore: [], + force: false, +} +const adminSession = {token: '', storeFqdn: ''} +describe('push', () => { beforeEach(() => { vi.mocked(uploadTheme).mockResolvedValue({ workPromise: Promise.resolve(), uploadResults: new Map(), renderThemeSyncProgress: () => Promise.resolve(), }) + vi.mocked(ensureThemeStore).mockReturnValue('example.myshopify.com') + vi.mocked(ensureAuthenticatedThemes).mockResolvedValue(adminSession) }) test('should call publishTheme if publish flag is provided', async () => { // Given const theme = buildTheme({id: 1, name: 'Theme', role: 'development'})! - vi.mocked(resolvePath).mockReturnValue('/provided/path') - vi.mocked(hasRequiredThemeDirectories).mockResolvedValue(true) - vi.mocked(fetchChecksums).mockResolvedValue([]) + vi.mocked(findOrSelectTheme).mockResolvedValue(theme) // When - await push(theme, adminSession, { - publish: true, - path: '', - nodelete: false, - json: false, - force: false, - }) + await push({...defaultFlags, publish: true}) // Then expect(publishTheme).toHaveBeenCalledWith(theme.id, adminSession) }) }) + +describe('createOrSelectTheme', () => { + test('creates unpublished theme when unpublished flag is provided', async () => { + // Given + vi.mocked(createTheme).mockResolvedValue(buildTheme({id: 2, name: 'Theme', role: UNPUBLISHED_THEME_ROLE})) + vi.mocked(fetchTheme).mockResolvedValue(undefined) + vi.mocked(promptThemeName).mockResolvedValue('Theme') + + const flags = {unpublished: true} + + // When + const theme = await createOrSelectTheme(adminSession, flags) + + // Then + expect(createTheme).toHaveBeenCalledWith({name: 'Theme', role: UNPUBLISHED_THEME_ROLE}, adminSession) + expect(theme).toMatchObject({role: UNPUBLISHED_THEME_ROLE}) + }) +}) From c7f90bb5279f5b9a35d48daaa93d628116fd32a6 Mon Sep 17 00:00:00 2001 From: James Meng Date: Mon, 23 Sep 2024 13:36:28 -0700 Subject: [PATCH 3/8] Export --- packages/theme/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/theme/src/index.ts b/packages/theme/src/index.ts index 105a4c81034..5c391d93713 100644 --- a/packages/theme/src/index.ts +++ b/packages/theme/src/index.ts @@ -15,6 +15,7 @@ import Rename from './cli/commands/theme/rename.js' import Serve from './cli/commands/theme/serve.js' import Share from './cli/commands/theme/share.js' import {pull} from './cli/services/pull.js' +import {push} from './cli/services/push.js' const COMMANDS = { 'theme:init': Init, @@ -37,6 +38,7 @@ const COMMANDS = { const PUBLIC_COMMANDS = { pull, + push, } export {PUBLIC_COMMANDS} From efc5c8893c2910e9006fc5f9b944430152995656 Mon Sep 17 00:00:00 2001 From: James Meng Date: Mon, 23 Sep 2024 13:47:28 -0700 Subject: [PATCH 4/8] Update JSDoc --- packages/theme/src/cli/services/push.ts | 103 +++++++++++++++--------- 1 file changed, 67 insertions(+), 36 deletions(-) diff --git a/packages/theme/src/cli/services/push.ts b/packages/theme/src/cli/services/push.ts index 3bb49c6b6d0..feb02783cae 100644 --- a/packages/theme/src/cli/services/push.ts +++ b/packages/theme/src/cli/services/push.ts @@ -51,48 +51,79 @@ interface JsonOutput { } export interface PushFlags { + /** The path to your theme directory. */ path?: string + + /** Password generated from the Theme Access app. */ password?: string + + /** Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com). */ store?: string + + /** The environment to apply to the current command. */ environment?: string + + /** Theme ID or name of the remote theme. */ theme?: string + + /** Push theme files from your remote development theme. */ development?: boolean + + /** Push theme files from your remote live theme. */ live?: boolean + + /** Create a new unpublished theme and push to it. */ unpublished?: boolean + + /** Runs the push command without deleting local files. */ nodelete?: boolean + + /** Download only the specified files (Multiple flags allowed). */ only?: string[] + + /** Skip downloading the specified files (Multiple flags allowed). */ ignore?: string[] + + /** Output JSON instead of a UI. */ json?: boolean + + /** Allow push to a live theme. */ allowLive?: boolean + + /** Publish as the live theme after uploading. */ publish?: boolean + + /** Proceed without confirmation, if current directory does not seem to be theme directory. */ force?: boolean + + /** Disable color output. */ noColor?: boolean + + /** Increase the verbosity of the output. */ verbose?: boolean } /** * Initiates the push process based on provided flags. * - * @param {PushFlags} flags - The flags for the push operation. - * @param {string} [flags.path] - The path to your theme directory. - * @param {string} [flags.password] - Password generated from the Theme Access app. - * @param {string} [flags.store] - Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com). - * @param {string} [flags.environment] - The environment to apply to the current command. - * @param {string} [flags.theme] - Theme ID or name of the remote theme. - * @param {boolean} [flags.development] - Push theme files from your remote development theme. - * @param {boolean} [flags.live] - Push theme files from your remote live theme. - * @param {boolean} [flags.unpublished] - Create a new unpublished theme and push to it. - * @param {boolean} [flags.nodelete] - Runs the push command without deleting local files. - * @param {string[]} [flags.only] - Download only the specified files (Multiple flags allowed). - * @param {string[]} [flags.ignore] - Skip downloading the specified files (Multiple flags allowed). - * @param {boolean} [flags.json] - Output JSON instead of a UI. - * @param {boolean} [flags.allowLive] - Allow push to a live theme. - * @param {boolean} [flags.publish] - Publish as the live theme after uploading. - * @param {boolean} [flags.legacy] - Use the legacy Ruby implementation for the `shopify theme push` command. - * @param {boolean} [flags.force] - Proceed without confirmation, if current directory does not seem to be theme directory. - * @param {boolean} [flags.noColor] - Disable color output. - * @param {boolean} [flags.verbose] - Increase the verbosity of the output. - * @returns {Promise} Resolves when the push operation is complete. + * @param flags - The flags for the push operation. + * @param flags.path - The path to your theme directory. + * @param flags.password - Password generated from the Theme Access app. + * @param flags.store - Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com). + * @param flags.environment - The environment to apply to the current command. + * @param flags.theme - Theme ID or name of the remote theme. + * @param flags.development - Push theme files from your remote development theme. + * @param flags.live - Push theme files from your remote live theme. + * @param flags.unpublished - Create a new unpublished theme and push to it. + * @param flags.nodelete - Runs the push command without deleting local files. + * @param flags.only - Download only the specified files (Multiple flags allowed). + * @param flags.ignore - Skip downloading the specified files (Multiple flags allowed). + * @param flags.json - Output JSON instead of a UI. + * @param flags.allowLive - Allow push to a live theme. + * @param flags.publish - Publish as the live theme after uploading. + * @param flags.force - Proceed without confirmation, if current directory does not seem to be theme directory. + * @param flags.noColor - Disable color output. + * @param flags.verbose - Increase the verbosity of the output. */ export async function push(flags: PushFlags) { const {path} = flags @@ -125,9 +156,9 @@ export async function push(flags: PushFlags) { /** * Executes the push operation for a given theme. * - * @param {Theme} theme - The theme to be pushed. - * @param {AdminSession} session - The admin session for the theme. - * @param {PushOptions} options - The options for the push operation. + * @param theme - The theme to be pushed. + * @param session - The admin session for the theme. + * @param options - The options for the push operation. * @returns {Promise} Resolves when the push operation is complete. */ async function executePush(theme: Theme, session: AdminSession, options: PushOptions) { @@ -154,7 +185,7 @@ async function executePush(theme: Theme, session: AdminSession, options: PushOpt /** * Checks if there are any upload errors in the results. * - * @param {Map} results - The map of upload results. + * @param results - The map of upload results. * @returns {boolean} - Returns true if there are any upload errors, otherwise false. */ function hasUploadErrors(results: Map): boolean { @@ -169,10 +200,10 @@ function hasUploadErrors(results: Map): boolean { /** * Handles the output based on the push operation results. * - * @param {Map} results - The map of upload results. - * @param {Theme} theme - The theme being pushed. - * @param {AdminSession} session - The admin session for the theme. - * @param {PushOptions} options - The options for the push operation. + * @param results - The map of upload results. + * @param theme - The theme being pushed. + * @param session - The admin session for the theme. + * @param options - The options for the push operation. * @returns {Promise} Resolves when the output handling is complete. */ async function handlePushOutput( @@ -195,9 +226,9 @@ async function handlePushOutput( /** * Handles the JSON output for the push operation. * - * @param {Theme} theme - The theme being pushed. - * @param {boolean} hasErrors - Indicates if there were any errors during the push operation. - * @param {AdminSession} session - The admin session for the theme. + * @param theme - The theme being pushed. + * @param hasErrors - Indicates if there were any errors during the push operation. + * @param session - The admin session for the theme. * @returns {void} */ function handleJsonOutput(theme: Theme, hasErrors: boolean, session: AdminSession) { @@ -222,8 +253,8 @@ function handleJsonOutput(theme: Theme, hasErrors: boolean, session: AdminSessio /** * Handles the output for the publish operation. * - * @param {boolean} hasErrors - Indicates if there were any errors during the push operation. - * @param {AdminSession} session - The admin session for the theme. + * @param hasErrors - Indicates if there were any errors during the push operation. + * @param session - The admin session for the theme. * @returns {void} */ function handlePublishOutput(hasErrors: boolean, session: AdminSession) { @@ -237,9 +268,9 @@ function handlePublishOutput(hasErrors: boolean, session: AdminSession) { /** * Handles the output for the push operation. * - * @param {Theme} theme - The theme being pushed. - * @param {boolean} hasErrors - Indicates if there were any errors during the push operation. - * @param {AdminSession} session - The admin session for the theme. + * @param theme - The theme being pushed. + * @param hasErrors - Indicates if there were any errors during the push operation. + * @param session - The admin session for the theme. * @returns {void} */ function handleOutput(theme: Theme, hasErrors: boolean, session: AdminSession) { From 076f64b16a8548bba34f17391382cc88fbaf880c Mon Sep 17 00:00:00 2001 From: James Meng Date: Mon, 23 Sep 2024 13:53:20 -0700 Subject: [PATCH 5/8] Add missing flags --- packages/theme/src/cli/commands/theme/push.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/theme/src/cli/commands/theme/push.ts b/packages/theme/src/cli/commands/theme/push.ts index 577dd14dc77..a29395292fb 100644 --- a/packages/theme/src/cli/commands/theme/push.ts +++ b/packages/theme/src/cli/commands/theme/push.ts @@ -143,12 +143,21 @@ export default class Push extends ThemeCommand { const pushFlags: PushFlags = { path: flags.path, password: flags.password, - environment: flags.environment, store: flags.store, + environment: flags.environment, theme: flags.theme, development: flags.development, live: flags.live, unpublished: flags.unpublished, + nodelete: flags.nodelete, + only: flags.only, + ignore: flags.ignore, + json: flags.json, + allowLive: flags['allow-live'], + publish: flags.publish, + force: flags.force, + noColor: flags['no-color'], + verbose: flags.verbose, } await push(pushFlags) From 8720f58bcb532477f92106d7cc31176f19d71bbe Mon Sep 17 00:00:00 2001 From: James Meng Date: Mon, 23 Sep 2024 14:06:39 -0700 Subject: [PATCH 6/8] Update tests --- packages/theme/src/cli/services/push.test.ts | 126 +++++++++++++++++-- packages/theme/src/cli/services/push.ts | 22 +--- 2 files changed, 121 insertions(+), 27 deletions(-) diff --git a/packages/theme/src/cli/services/push.test.ts b/packages/theme/src/cli/services/push.test.ts index 7344b348c50..78513c000f7 100644 --- a/packages/theme/src/cli/services/push.test.ts +++ b/packages/theme/src/cli/services/push.test.ts @@ -1,5 +1,6 @@ -import {createOrSelectTheme, push} from './push.js' +import {createOrSelectTheme, push, ThemeSelectionOptions} from './push.js' import {PullFlags} from './pull.js' +import {setDevelopmentTheme} from './local-storage.js' import {uploadTheme} from '../utilities/theme-uploader.js' import {ensureThemeStore} from '../utilities/theme-store.js' import {findOrSelectTheme} from '../utilities/theme-selector.js' @@ -7,16 +8,22 @@ import {buildTheme} from '@shopify/cli-kit/node/themes/factories' import {test, describe, vi, expect, beforeEach} from 'vitest' import {createTheme, fetchTheme, publishTheme} from '@shopify/cli-kit/node/themes/api' import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' -import {promptThemeName, UNPUBLISHED_THEME_ROLE} from '@shopify/cli-kit/node/themes/utils' +import { + DEVELOPMENT_THEME_ROLE, + LIVE_THEME_ROLE, + promptThemeName, + UNPUBLISHED_THEME_ROLE, +} from '@shopify/cli-kit/node/themes/utils' +import {renderConfirmationPrompt} from '@shopify/cli-kit/node/ui' -vi.mock('../utilities/theme-fs.js') vi.mock('../utilities/theme-uploader.js') vi.mock('../utilities/theme-store.js') vi.mock('../utilities/theme-selector.js') +vi.mock('./local-storage.js') vi.mock('@shopify/cli-kit/node/themes/utils') vi.mock('@shopify/cli-kit/node/session') -vi.mock('@shopify/cli-kit/node/path') vi.mock('@shopify/cli-kit/node/themes/api') +vi.mock('@shopify/cli-kit/node/ui') const path = '/my-theme' const defaultFlags: PullFlags = { @@ -54,20 +61,123 @@ describe('push', () => { }) }) -describe('createOrSelectTheme', () => { +describe('createOrSelectTheme', async () => { test('creates unpublished theme when unpublished flag is provided', async () => { // Given vi.mocked(createTheme).mockResolvedValue(buildTheme({id: 2, name: 'Theme', role: UNPUBLISHED_THEME_ROLE})) vi.mocked(fetchTheme).mockResolvedValue(undefined) - vi.mocked(promptThemeName).mockResolvedValue('Theme') - const flags = {unpublished: true} + const flags: ThemeSelectionOptions = {unpublished: true} // When const theme = await createOrSelectTheme(adminSession, flags) // Then - expect(createTheme).toHaveBeenCalledWith({name: 'Theme', role: UNPUBLISHED_THEME_ROLE}, adminSession) expect(theme).toMatchObject({role: UNPUBLISHED_THEME_ROLE}) + expect(setDevelopmentTheme).not.toHaveBeenCalled() + }) + + test('creates development theme when development flag is provided', async () => { + // Given + vi.mocked(createTheme).mockResolvedValue(buildTheme({id: 1, name: 'Theme', role: DEVELOPMENT_THEME_ROLE})) + vi.mocked(fetchTheme).mockResolvedValue(undefined) + const flags: ThemeSelectionOptions = {development: true} + + // When + const theme = await createOrSelectTheme(adminSession, flags) + + // Then + expect(theme).toMatchObject({role: DEVELOPMENT_THEME_ROLE}) + expect(setDevelopmentTheme).toHaveBeenCalled() + }) + + test('creates development theme when development and unpublished flags are provided', async () => { + // Given + vi.mocked(createTheme).mockResolvedValue(buildTheme({id: 1, name: 'Theme', role: DEVELOPMENT_THEME_ROLE})) + vi.mocked(fetchTheme).mockResolvedValue(undefined) + const flags: ThemeSelectionOptions = {development: true, unpublished: true} + + // When + const theme = await createOrSelectTheme(adminSession, flags) + + // Then + expect(theme).toMatchObject({role: DEVELOPMENT_THEME_ROLE}) + }) + + test('returns live theme when live flag is provided', async () => { + // Given + vi.mocked(findOrSelectTheme).mockResolvedValue(buildTheme({id: 3, name: 'Live Theme', role: LIVE_THEME_ROLE})!) + const flags: ThemeSelectionOptions = {live: true, 'allow-live': true} + + // When + const theme = await createOrSelectTheme(adminSession, flags) + + // Then + expect(theme).toMatchObject({role: LIVE_THEME_ROLE}) + }) + + test("renders confirmation prompt if 'allow-live' flag is not provided and selected theme role is live", async () => { + // Given + vi.mocked(findOrSelectTheme).mockResolvedValue(buildTheme({id: 3, name: 'Live Theme', role: LIVE_THEME_ROLE})!) + vi.mocked(renderConfirmationPrompt).mockResolvedValue(true) + const flags: ThemeSelectionOptions = {live: true} + + // When + const theme = await createOrSelectTheme(adminSession, flags) + + // Then + expect(theme?.role).toBe(LIVE_THEME_ROLE) + expect(renderConfirmationPrompt).toHaveBeenCalled() + }) + + test("renders confirmation prompt if 'allow-live' flag is not provided and live theme is specified via theme flag", async () => { + // Given + vi.mocked(findOrSelectTheme).mockResolvedValue(buildTheme({id: 3, name: 'Live Theme', role: LIVE_THEME_ROLE})!) + vi.mocked(renderConfirmationPrompt).mockResolvedValue(true) + const flags: ThemeSelectionOptions = {theme: '3'} + + // When + const theme = await createOrSelectTheme(adminSession, flags) + + // Then + expect(theme?.role).toBe(LIVE_THEME_ROLE) + expect(renderConfirmationPrompt).toHaveBeenCalled() + }) + + test('returns undefined if live theme confirmation prompt is not confirmed', async () => { + // Given + vi.mocked(findOrSelectTheme).mockResolvedValue(buildTheme({id: 3, name: 'Live Theme', role: LIVE_THEME_ROLE})!) + vi.mocked(renderConfirmationPrompt).mockResolvedValue(false) + const flags: ThemeSelectionOptions = {live: true} + + // When + const theme = await createOrSelectTheme(adminSession, flags) + + // Then + expect(theme).toBeUndefined() + }) + + test('returns undefined if confirmation prompt is rejected', async () => { + // Given + vi.mocked(findOrSelectTheme).mockResolvedValue(buildTheme({id: 3, name: 'Live Theme', role: LIVE_THEME_ROLE})!) + vi.mocked(renderConfirmationPrompt).mockResolvedValue(false) + const flags = {live: true} + + // When + const theme = await createOrSelectTheme(adminSession, flags) + + // Then + expect(theme).toBeUndefined() + }) + + test('renders text prompt if unpublished flag is provided and theme flag is not provided', async () => { + // Given + const flags = {unpublished: true} + + // When + await createOrSelectTheme(adminSession, flags) + + // Then + expect(promptThemeName).toHaveBeenCalledWith('Name of the new theme') }) }) diff --git a/packages/theme/src/cli/services/push.ts b/packages/theme/src/cli/services/push.ts index feb02783cae..3e95bbf6576 100644 --- a/packages/theme/src/cli/services/push.ts +++ b/packages/theme/src/cli/services/push.ts @@ -20,7 +20,7 @@ import {themeEditorUrl, themePreviewUrl} from '@shopify/cli-kit/node/themes/urls import {cwd, resolvePath} from '@shopify/cli-kit/node/path' import {LIVE_THEME_ROLE, promptThemeName, UNPUBLISHED_THEME_ROLE} from '@shopify/cli-kit/node/themes/utils' -interface ThemeSelectionOptions { +export interface ThemeSelectionOptions { live?: boolean development?: boolean unpublished?: boolean @@ -107,25 +107,9 @@ export interface PushFlags { * Initiates the push process based on provided flags. * * @param flags - The flags for the push operation. - * @param flags.path - The path to your theme directory. - * @param flags.password - Password generated from the Theme Access app. - * @param flags.store - Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com). - * @param flags.environment - The environment to apply to the current command. - * @param flags.theme - Theme ID or name of the remote theme. - * @param flags.development - Push theme files from your remote development theme. - * @param flags.live - Push theme files from your remote live theme. - * @param flags.unpublished - Create a new unpublished theme and push to it. - * @param flags.nodelete - Runs the push command without deleting local files. - * @param flags.only - Download only the specified files (Multiple flags allowed). - * @param flags.ignore - Skip downloading the specified files (Multiple flags allowed). - * @param flags.json - Output JSON instead of a UI. - * @param flags.allowLive - Allow push to a live theme. - * @param flags.publish - Publish as the live theme after uploading. - * @param flags.force - Proceed without confirmation, if current directory does not seem to be theme directory. - * @param flags.noColor - Disable color output. - * @param flags.verbose - Increase the verbosity of the output. + * @returns {Promise} Resolves when the push operation is complete. */ -export async function push(flags: PushFlags) { +export async function push(flags: PushFlags): Promise { const {path} = flags const force = flags.force ?? false From b09d3de7e4d87b3e9d22d44a8e0602f01a48c2a1 Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Mon, 30 Sep 2024 12:58:26 +0200 Subject: [PATCH 7/8] Update the docs to be cohesive with the 'pull' command docs --- packages/theme/src/cli/services/push.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/theme/src/cli/services/push.ts b/packages/theme/src/cli/services/push.ts index 3e95bbf6576..be35e12fb9f 100644 --- a/packages/theme/src/cli/services/push.ts +++ b/packages/theme/src/cli/services/push.ts @@ -107,7 +107,6 @@ export interface PushFlags { * Initiates the push process based on provided flags. * * @param flags - The flags for the push operation. - * @returns {Promise} Resolves when the push operation is complete. */ export async function push(flags: PushFlags): Promise { const {path} = flags @@ -138,12 +137,11 @@ export async function push(flags: PushFlags): Promise { } /** - * Executes the push operation for a given theme. + * Executes the push operation for a specific theme. * - * @param theme - The theme to be pushed. - * @param session - The admin session for the theme. - * @param options - The options for the push operation. - * @returns {Promise} Resolves when the push operation is complete. + * @param theme - the remote theme to be updated by the push command + * @param session - the admin session to access the API and upload the theme + * @param options - the options that modify how the theme gets uploaded */ async function executePush(theme: Theme, session: AdminSession, options: PushOptions) { const themeChecksums = await fetchChecksums(theme.id, session) From 8c83600a9c9c61a69b31d3ae2a1d91e30df4e94e Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Mon, 30 Sep 2024 13:00:12 +0200 Subject: [PATCH 8/8] Update the docs to be cohesive with the 'pull' command docs [2] --- packages/theme/src/cli/services/push.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/theme/src/cli/services/push.ts b/packages/theme/src/cli/services/push.ts index be35e12fb9f..8e3943c00ee 100644 --- a/packages/theme/src/cli/services/push.ts +++ b/packages/theme/src/cli/services/push.ts @@ -186,7 +186,6 @@ function hasUploadErrors(results: Map): boolean { * @param theme - The theme being pushed. * @param session - The admin session for the theme. * @param options - The options for the push operation. - * @returns {Promise} Resolves when the output handling is complete. */ async function handlePushOutput( results: Map, @@ -211,7 +210,6 @@ async function handlePushOutput( * @param theme - The theme being pushed. * @param hasErrors - Indicates if there were any errors during the push operation. * @param session - The admin session for the theme. - * @returns {void} */ function handleJsonOutput(theme: Theme, hasErrors: boolean, session: AdminSession) { const output: JsonOutput = { @@ -237,7 +235,6 @@ function handleJsonOutput(theme: Theme, hasErrors: boolean, session: AdminSessio * * @param hasErrors - Indicates if there were any errors during the push operation. * @param session - The admin session for the theme. - * @returns {void} */ function handlePublishOutput(hasErrors: boolean, session: AdminSession) { if (hasErrors) { @@ -253,7 +250,6 @@ function handlePublishOutput(hasErrors: boolean, session: AdminSession) { * @param theme - The theme being pushed. * @param hasErrors - Indicates if there were any errors during the push operation. * @param session - The admin session for the theme. - * @returns {void} */ function handleOutput(theme: Theme, hasErrors: boolean, session: AdminSession) { const nextSteps = [