From d64a9a13d4b1dc3578c3a347e77512bcd724f9a0 Mon Sep 17 00:00:00 2001 From: James Meng <35415298+jamesmengo@users.noreply.github.com> Date: Thu, 17 Oct 2024 12:16:56 -0700 Subject: [PATCH] Revert "[Themes] Remove Ruby from `theme push` command" --- packages/cli/oclif.manifest.json | 22 ++++ .../theme/src/cli/commands/theme/push.test.ts | 114 ++++++++++++++++++ packages/theme/src/cli/commands/theme/push.ts | 77 +++++++++++- 3 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 packages/theme/src/cli/commands/theme/push.test.ts diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 86e72c96ef..3daed03050 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -5760,6 +5760,20 @@ ], "args": { }, + "cli2Flags": [ + "theme", + "development", + "live", + "unpublished", + "nodelete", + "only", + "ignore", + "json", + "allow-live", + "publish", + "force", + "development-theme-id" + ], "customPluginName": "@shopify/theme", "description": "Uploads your local theme files to Shopify, overwriting the remote version if specified.\n\n If no theme is specified, then you're prompted to select the theme to overwrite from the list of the themes in your store.\n\n You can run this command only in a directory that matches the \"default Shopify theme folder structure\" (https://shopify.dev/docs/themes/tools/cli#directory-structure).\n\n This command returns the following information:\n\n - A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.\n\n If you use the `--json` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.\n\n Sample output:\n\n ```json\n {\n \"theme\": {\n \"id\": 108267175958,\n \"name\": \"MyTheme\",\n \"role\": \"unpublished\",\n \"shop\": \"mystore.myshopify.com\",\n \"editor_url\": \"https://mystore.myshopify.com/admin/themes/108267175958/editor\",\n \"preview_url\": \"https://mystore.myshopify.com/?preview_theme_id=108267175958\"\n }\n }\n ```\n ", "descriptionWithMarkdown": "Uploads your local theme files to Shopify, overwriting the remote version if specified.\n\n If no theme is specified, then you're prompted to select the theme to overwrite from the list of the themes in your store.\n\n You can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure).\n\n This command returns the following information:\n\n - A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.\n\n If you use the `--json` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.\n\n Sample output:\n\n ```json\n {\n \"theme\": {\n \"id\": 108267175958,\n \"name\": \"MyTheme\",\n \"role\": \"unpublished\",\n \"shop\": \"mystore.myshopify.com\",\n \"editor_url\": \"https://mystore.myshopify.com/admin/themes/108267175958/editor\",\n \"preview_url\": \"https://mystore.myshopify.com/?preview_theme_id=108267175958\"\n }\n }\n ```\n ", @@ -5815,6 +5829,14 @@ "name": "json", "type": "boolean" }, + "legacy": { + "allowNo": false, + "description": "Use the legacy Ruby implementation for the `shopify theme push` command.", + "env": "SHOPIFY_FLAG_LEGACY", + "hidden": true, + "name": "legacy", + "type": "boolean" + }, "live": { "allowNo": false, "char": "l", diff --git a/packages/theme/src/cli/commands/theme/push.test.ts b/packages/theme/src/cli/commands/theme/push.test.ts new file mode 100644 index 0000000000..e0fa013615 --- /dev/null +++ b/packages/theme/src/cli/commands/theme/push.test.ts @@ -0,0 +1,114 @@ +import Push from './push.js' +import {DevelopmentThemeManager} from '../../utilities/development-theme-manager.js' +import {ensureThemeStore} from '../../utilities/theme-store.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' + +vi.mock('../../services/push.js') +vi.mock('../../utilities/theme-store.js') +vi.mock('../../utilities/theme-selector.js') +vi.mock('../../services/local-storage.js') +vi.mock('@shopify/cli-kit/node/ruby') +vi.mock('@shopify/cli-kit/node/session') +vi.mock('@shopify/cli-kit/node/themes/api') +vi.mock('@shopify/cli-kit/node/ui') + +const CommandConfig = new Config({root: __dirname}) + +describe('Push', () => { + const adminSession = {token: '', storeFqdn: ''} + const path = '/my-theme' + + beforeEach(() => { + vi.mocked(getDevelopmentTheme).mockImplementation(() => undefined) + vi.mocked(setDevelopmentTheme).mockImplementation(() => undefined) + vi.mocked(removeDevelopmentTheme).mockImplementation(() => undefined) + }) + + describe('run with Ruby implementation', () => { + test('should pass development theme from local storage to Ruby implementation', async () => { + // Given + const theme = buildTheme({id: 1, name: 'Theme', role: 'development'})! + vi.spyOn(DevelopmentThemeManager.prototype, 'findOrCreate').mockResolvedValue(theme) + vi.spyOn(DevelopmentThemeManager.prototype, 'fetch').mockResolvedValue(theme) + await run([]) + + // Then + expect(DevelopmentThemeManager.prototype.findOrCreate).not.toHaveBeenCalled() + expect(DevelopmentThemeManager.prototype.fetch).toHaveBeenCalledOnce() + expectCLI2ToHaveBeenCalledWith(`theme push ${path} --development-theme-id ${theme.id}`) + }) + + test('should pass theme and development theme from local storage to Ruby implementation', async () => { + // Given + const themeId = 2 + const theme = buildTheme({id: 3, name: 'Theme', role: 'development'})! + vi.spyOn(DevelopmentThemeManager.prototype, 'findOrCreate').mockResolvedValue(theme) + vi.spyOn(DevelopmentThemeManager.prototype, 'fetch').mockResolvedValue(theme) + await run([`--theme=${themeId}`]) + + // Then + expectCLI2ToHaveBeenCalledWith(`theme push ${path} --theme ${themeId} --development-theme-id ${theme.id}`) + }) + + test('should not pass development theme to Ruby implementation if local storage is empty', async () => { + // When + await run([]) + + // Then + expect(DevelopmentThemeManager.prototype.findOrCreate).not.toHaveBeenCalled() + expect(DevelopmentThemeManager.prototype.fetch).toHaveBeenCalledOnce() + expectCLI2ToHaveBeenCalledWith(`theme push ${path}`) + }) + + test('should pass theme and development theme to Ruby implementation', async () => { + // Given + const theme = buildTheme({id: 4, name: 'Theme', role: 'development'})! + vi.spyOn(DevelopmentThemeManager.prototype, 'findOrCreate').mockResolvedValue(theme) + vi.spyOn(DevelopmentThemeManager.prototype, 'fetch').mockResolvedValue(theme) + await run(['--development']) + + // Then + expect(DevelopmentThemeManager.prototype.findOrCreate).toHaveBeenCalledOnce() + 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[]) { + await runPushCommand(['--legacy', ...argv], path, adminSession) + } + + function expectCLI2ToHaveBeenCalledWith(command: string) { + expect(execCLI2).toHaveBeenCalledWith(command.split(' '), { + store: 'example.myshopify.com', + adminToken: adminSession.token, + }) + } + + async function runPushCommand(argv: string[], path: string, adminSession: AdminSession) { + vi.mocked(ensureThemeStore).mockReturnValue('example.myshopify.com') + vi.mocked(ensureAuthenticatedThemes).mockResolvedValue(adminSession) + + await CommandConfig.load() + const push = new Push([`--path=${path}`, ...argv], CommandConfig) + + await push.run() + } +}) diff --git a/packages/theme/src/cli/commands/theme/push.ts b/packages/theme/src/cli/commands/theme/push.ts index 4d9f9950e6..d256e7c1c8 100644 --- a/packages/theme/src/cli/commands/theme/push.ts +++ b/packages/theme/src/cli/commands/theme/push.ts @@ -1,8 +1,17 @@ import {themeFlags} from '../../flags.js' -import ThemeCommand from '../../utilities/theme-command.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 {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 {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' +import {cwd, resolvePath} from '@shopify/cli-kit/node/path' +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.' @@ -95,6 +104,11 @@ export default class Push extends ThemeCommand { description: 'Publish as the live theme after uploading.', env: 'SHOPIFY_FLAG_PUBLISH', }), + legacy: Flags.boolean({ + hidden: true, + description: 'Use the legacy Ruby implementation for the `shopify theme push` command.', + env: 'SHOPIFY_FLAG_LEGACY', + }), force: Flags.boolean({ hidden: true, char: 'f', @@ -103,9 +117,29 @@ export default class Push extends ThemeCommand { }), } + static cli2Flags = [ + 'theme', + 'development', + 'live', + 'unpublished', + 'nodelete', + 'only', + 'ignore', + 'json', + 'allow-live', + 'publish', + 'force', + 'development-theme-id', + ] + async run(): Promise { const {flags} = await this.parse(Push) + if (flags.password || flags.legacy) { + await this.execLegacyPush() + return + } + const pushFlags: PushFlags = { path: flags.path, password: flags.password, @@ -128,4 +162,45 @@ export default class Push extends ThemeCommand { await push(pushFlags) } + + async execLegacyPush() { + const {flags} = await this.parse(Push) + const path = flags.path || cwd() + 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 flagsForCli2 = flags as typeof flags & FlagValues + + showEmbeddedCLIWarning() + + const developmentThemeManager = new DevelopmentThemeManager(adminSession) + + const targetTheme = await (flagsForCli2.development + ? developmentThemeManager.findOrCreate() + : developmentThemeManager.fetch()) + + if (targetTheme) { + if (flagsForCli2.development) { + flagsForCli2.theme = `${targetTheme.id}` + flagsForCli2.development = false + } + if (useEmbeddedThemeCLI()) { + flagsForCli2['development-theme-id'] = targetTheme.id + } + } + + const flagsToPass = this.passThroughFlags(flagsForCli2, { + allowedFlags: Push.cli2Flags, + }) + const command = ['theme', 'push', flagsForCli2.path, ...flagsToPass] + + await execCLI2(command, {store, adminToken: adminSession.token}) + } }