Skip to content

Commit

Permalink
Merge pull request #4501 from Shopify/jm/api-push
Browse files Browse the repository at this point in the history
[Themes] - Refactor `push` command to prepare for public themes API
  • Loading branch information
karreiro authored Sep 30, 2024
2 parents 086f2e4 + 8c83600 commit c9d8fbb
Show file tree
Hide file tree
Showing 5 changed files with 412 additions and 269 deletions.
170 changes: 13 additions & 157 deletions packages/theme/src/cli/commands/theme/push.test.ts
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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
Expand Down Expand Up @@ -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[]) {
Expand Down
125 changes: 37 additions & 88 deletions packages/theme/src/cli/commands/theme/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand Down Expand Up @@ -139,33 +134,45 @@ export default class Push extends ThemeCommand {

async run(): Promise<void> {
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,
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,
}

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
}

Expand Down Expand Up @@ -197,61 +204,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<Theme | undefined> {
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
}
Loading

0 comments on commit c9d8fbb

Please sign in to comment.