diff --git a/.changeset/khaki-snakes-stare.md b/.changeset/khaki-snakes-stare.md new file mode 100644 index 00000000000..64fb7a256f3 --- /dev/null +++ b/.changeset/khaki-snakes-stare.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli-kit': minor +--- + +Use GraphQL for theme creation diff --git a/packages/app/src/cli/utilities/extensions/theme/host-theme-manager.test.ts b/packages/app/src/cli/utilities/extensions/theme/host-theme-manager.test.ts index 858297f61c7..2524a1db019 100644 --- a/packages/app/src/cli/utilities/extensions/theme/host-theme-manager.test.ts +++ b/packages/app/src/cli/utilities/extensions/theme/host-theme-manager.test.ts @@ -1,6 +1,6 @@ import {waitForThemeToBeProcessed} from './host-theme-watcher.js' import {HostThemeManager, DEFAULT_THEME_ZIP, FALLBACK_THEME_ZIP} from './host-theme-manager.js' -import {createTheme} from '@shopify/cli-kit/node/themes/api' +import {themeCreate} from '@shopify/cli-kit/node/themes/api' import {beforeEach, describe, expect, test, vi} from 'vitest' import {DEVELOPMENT_THEME_ROLE} from '@shopify/cli-kit/node/themes/utils' import {AdminSession} from '@shopify/cli-kit/node/session' @@ -18,8 +18,8 @@ describe('HostThemeManager', () => { vi.spyOn(ThemeManager.prototype, 'generateThemeName').mockImplementation(() => 'App Ext. Host Name') }) - test('should call createTheme with the provided name and src param', async () => { - vi.mocked(createTheme).mockResolvedValue({ + test('should call themeCreate with the provided name and src param', async () => { + vi.mocked(themeCreate).mockResolvedValue({ id: 12345, name: 'Theme', role: 'development', @@ -31,7 +31,7 @@ describe('HostThemeManager', () => { await themeManager.findOrCreate() // Then - expect(createTheme).toHaveBeenCalledWith( + expect(themeCreate).toHaveBeenCalledWith( { name: 'App Ext. Host Name', role: DEVELOPMENT_THEME_ROLE, @@ -42,9 +42,9 @@ describe('HostThemeManager', () => { }) describe('dev preview', () => { - test('should call createTheme with the provided name and src param', async () => { + test('should call themeCreate with the provided name and src param', async () => { // Given - vi.mocked(createTheme).mockResolvedValue({ + vi.mocked(themeCreate).mockResolvedValue({ id: 12345, name: 'Theme', role: 'development', @@ -56,7 +56,7 @@ describe('HostThemeManager', () => { await themeManager.findOrCreate() // Then - expect(createTheme).toHaveBeenCalledWith( + expect(themeCreate).toHaveBeenCalledWith( { name: 'App Ext. Host Name', role: DEVELOPMENT_THEME_ROLE, @@ -68,7 +68,7 @@ describe('HostThemeManager', () => { test('should wait for the theme to be processed', async () => { // Given - vi.mocked(createTheme).mockResolvedValue({ + vi.mocked(themeCreate).mockResolvedValue({ id: 12345, name: 'Theme', role: 'development', @@ -86,7 +86,7 @@ describe('HostThemeManager', () => { test('should retry creating the theme if the first attempt fails', async () => { // Given - vi.mocked(createTheme).mockResolvedValueOnce(undefined).mockResolvedValueOnce({ + vi.mocked(themeCreate).mockResolvedValueOnce(undefined).mockResolvedValueOnce({ id: 12345, name: 'Theme', role: 'development', @@ -98,8 +98,8 @@ describe('HostThemeManager', () => { await themeManager.findOrCreate() // Then - expect(createTheme).toHaveBeenCalledTimes(2) - expect(createTheme).toHaveBeenNthCalledWith( + expect(themeCreate).toHaveBeenCalledTimes(2) + expect(themeCreate).toHaveBeenNthCalledWith( 1, { role: DEVELOPMENT_THEME_ROLE, @@ -108,7 +108,7 @@ describe('HostThemeManager', () => { }, adminSession, ) - expect(createTheme).toHaveBeenNthCalledWith( + expect(themeCreate).toHaveBeenNthCalledWith( 2, { role: DEVELOPMENT_THEME_ROLE, @@ -121,7 +121,7 @@ describe('HostThemeManager', () => { test('should gracefully handle a 422 from the server during theme creation', async () => { // Given - vi.mocked(createTheme) + vi.mocked(themeCreate) .mockRejectedValueOnce(new Error('API request unprocessable content: {"src":["is empty"]}')) .mockRejectedValueOnce(new Error('API request unprocessable content: {"src":["is empty"]}')) .mockResolvedValueOnce({ @@ -136,12 +136,12 @@ describe('HostThemeManager', () => { await themeManager.findOrCreate() // Then - expect(createTheme).toHaveBeenCalledTimes(3) + expect(themeCreate).toHaveBeenCalledTimes(3) }) test('should retry creating the theme with the Fallback theme zip after 3 failed retry attempts', async () => { // Given - vi.mocked(createTheme) + vi.mocked(themeCreate) .mockResolvedValueOnce(undefined) .mockResolvedValueOnce(undefined) .mockResolvedValueOnce(undefined) @@ -157,8 +157,8 @@ describe('HostThemeManager', () => { await themeManager.findOrCreate() // Then - expect(createTheme).toHaveBeenCalledTimes(4) - expect(createTheme).toHaveBeenLastCalledWith( + expect(themeCreate).toHaveBeenCalledTimes(4) + expect(themeCreate).toHaveBeenLastCalledWith( { role: DEVELOPMENT_THEME_ROLE, name: 'App Ext. Host Name', @@ -170,14 +170,14 @@ describe('HostThemeManager', () => { test('should throw a BugError if the theme cannot be created', async () => { // Given - vi.mocked(createTheme).mockResolvedValue(undefined) + vi.mocked(themeCreate).mockResolvedValue(undefined) // When // Then await expect(themeManager.findOrCreate()).rejects.toThrow( 'Could not create theme with name "App Ext. Host Name" and role "development"', ) - expect(createTheme).toHaveBeenCalledTimes(4) + expect(themeCreate).toHaveBeenCalledTimes(4) }) }) }) diff --git a/packages/app/src/cli/utilities/extensions/theme/host-theme-manager.ts b/packages/app/src/cli/utilities/extensions/theme/host-theme-manager.ts index 9c55f907bd9..2122e2951fb 100644 --- a/packages/app/src/cli/utilities/extensions/theme/host-theme-manager.ts +++ b/packages/app/src/cli/utilities/extensions/theme/host-theme-manager.ts @@ -3,7 +3,7 @@ import {getHostTheme, removeHostTheme, setHostTheme} from '@shopify/cli-kit/node import {ThemeManager} from '@shopify/cli-kit/node/themes/theme-manager' import {AdminSession} from '@shopify/cli-kit/node/session' import {Theme} from '@shopify/cli-kit/node/themes/types' -import {createTheme} from '@shopify/cli-kit/node/themes/api' +import {themeCreate} from '@shopify/cli-kit/node/themes/api' import {DEVELOPMENT_THEME_ROLE} from '@shopify/cli-kit/node/themes/utils' import {BugError} from '@shopify/cli-kit/node/error' import {outputDebug} from '@shopify/cli-kit/node/output' @@ -52,7 +52,7 @@ export class HostThemeManager extends ThemeManager { try { // eslint-disable-next-line no-await-in-loop - const theme = await createTheme(options, this.adminSession) + const theme = await themeCreate(options, this.adminSession) if (theme) { this.setTheme(theme.id.toString()) outputDebug(`Waiting for theme with id "${theme.id}" to be processed`) @@ -69,7 +69,7 @@ export class HostThemeManager extends ThemeManager { } outputDebug(`Theme creation failed after ${retryAttemps} retries. Creating theme using fallback theme zip`) - const theme = await createTheme({...options, src: FALLBACK_THEME_ZIP}, this.adminSession) + const theme = await themeCreate({...options, src: FALLBACK_THEME_ZIP}, this.adminSession) if (!theme) { outputDebug(`Theme creation failed. Exiting process.`) throw new BugError(`Could not create theme with name "${options.name}" and role "${options.role}"`) diff --git a/packages/cli-kit/src/cli/api/graphql/admin/generated/theme_create.ts b/packages/cli-kit/src/cli/api/graphql/admin/generated/theme_create.ts new file mode 100644 index 00000000000..41cb3351298 --- /dev/null +++ b/packages/cli-kit/src/cli/api/graphql/admin/generated/theme_create.ts @@ -0,0 +1,102 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ +import * as Types from './types.js' + +import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' + +export type ThemeCreateMutationVariables = Types.Exact<{ + name: Types.Scalars['String']['input'] + source: Types.Scalars['URL']['input'] + role: Types.ThemeRole +}> + +export type ThemeCreateMutation = { + themeCreate?: { + theme?: {id: string; name: string; role: Types.ThemeRole} | null + userErrors: {field?: string[] | null; message: string}[] + } | null +} + +export const ThemeCreate = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: {kind: 'Name', value: 'themeCreate'}, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: {kind: 'Variable', name: {kind: 'Name', value: 'name'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, + }, + { + kind: 'VariableDefinition', + variable: {kind: 'Variable', name: {kind: 'Name', value: 'source'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'URL'}}}, + }, + { + kind: 'VariableDefinition', + variable: {kind: 'Variable', name: {kind: 'Name', value: 'role'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ThemeRole'}}}, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: {kind: 'Name', value: 'themeCreate'}, + arguments: [ + { + kind: 'Argument', + name: {kind: 'Name', value: 'name'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'name'}}, + }, + { + kind: 'Argument', + name: {kind: 'Name', value: 'source'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'source'}}, + }, + { + kind: 'Argument', + name: {kind: 'Name', value: 'role'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'role'}}, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: {kind: 'Name', value: 'theme'}, + selectionSet: { + kind: 'SelectionSet', + selections: [ + {kind: 'Field', name: {kind: 'Name', value: 'id'}}, + {kind: 'Field', name: {kind: 'Name', value: 'name'}}, + {kind: 'Field', name: {kind: 'Name', value: 'role'}}, + {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, + ], + }, + }, + { + kind: 'Field', + name: {kind: 'Name', value: 'userErrors'}, + selectionSet: { + kind: 'SelectionSet', + selections: [ + {kind: 'Field', name: {kind: 'Name', value: 'field'}}, + {kind: 'Field', name: {kind: 'Name', value: 'message'}}, + {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, + ], + }, + }, + {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode diff --git a/packages/cli-kit/src/cli/api/graphql/admin/mutations/theme_create.graphql b/packages/cli-kit/src/cli/api/graphql/admin/mutations/theme_create.graphql new file mode 100644 index 00000000000..cb3b713f9b4 --- /dev/null +++ b/packages/cli-kit/src/cli/api/graphql/admin/mutations/theme_create.graphql @@ -0,0 +1,13 @@ +mutation themeCreate($name: String!, $source: URL!, $role: ThemeRole!) { + themeCreate(name: $name, source: $source, role: $role) { + theme { + id + name + role + } + userErrors { + field + message + } + } +} diff --git a/packages/cli-kit/src/public/node/themes/api.test.ts b/packages/cli-kit/src/public/node/themes/api.test.ts index debb3d9741c..475daeb59c1 100644 --- a/packages/cli-kit/src/public/node/themes/api.test.ts +++ b/packages/cli-kit/src/public/node/themes/api.test.ts @@ -1,5 +1,5 @@ import { - createTheme, + themeCreate, themeDelete, fetchTheme, fetchThemes, @@ -15,6 +15,7 @@ import {Operation} from './types.js' import {ThemeDelete} from '../../../cli/api/graphql/admin/generated/theme_delete.js' import {ThemeUpdate} from '../../../cli/api/graphql/admin/generated/theme_update.js' import {ThemePublish} from '../../../cli/api/graphql/admin/generated/theme_publish.js' +import {ThemeCreate} from '../../../cli/api/graphql/admin/generated/theme_create.js' import {GetThemeFileChecksums} from '../../../cli/api/graphql/admin/generated/get_theme_file_checksums.js' import {ThemeFilesUpsert} from '../../../cli/api/graphql/admin/generated/theme_files_upsert.js' import {ThemeFilesDelete} from '../../../cli/api/graphql/admin/generated/theme_files_delete.js' @@ -22,7 +23,7 @@ import {OnlineStoreThemeFileBodyInputType} from '../../../cli/api/graphql/admin/ import {GetThemes} from '../../../cli/api/graphql/admin/generated/get_themes.js' import {GetTheme} from '../../../cli/api/graphql/admin/generated/get_theme.js' import {test, vi, expect, describe} from 'vitest' -import {adminRequestDoc, restRequest, supportedApiVersions} from '@shopify/cli-kit/node/api/admin' +import {adminRequestDoc, supportedApiVersions} from '@shopify/cli-kit/node/api/admin' import {AbortError} from '@shopify/cli-kit/node/error' import {ClientError} from 'graphql-request' @@ -155,33 +156,39 @@ describe('fetchChecksums', () => { }) }) -describe('createTheme', () => { +describe('themeCreate', () => { const id = 123 const name = 'new theme' const role = 'unpublished' - const processing = false const params: ThemeParams = {name, role} test('creates a theme', async () => { // Given - vi.mocked(restRequest).mockResolvedValueOnce({ - json: {theme: {id, name, role, processing}}, - status: 200, - headers: {}, - }) - - vi.mocked(adminRequestDoc).mockResolvedValue({ - themeFilesUpsert: { - upsertedThemeFiles: [], + vi.mocked(adminRequestDoc).mockResolvedValueOnce({ + themeCreate: { + theme: { + id: `gid://shopify/OnlineStoreTheme/${id}`, + name, + role, + }, userErrors: [], }, }) // When - const theme = await createTheme(params, session) + const theme = await themeCreate(params, session) // Then - expect(restRequest).toHaveBeenCalledWith('POST', '/themes', session, {theme: params}, {}) + expect(adminRequestDoc).toHaveBeenCalledWith( + ThemeCreate, + session, + { + name: params.name, + source: 'https://cdn.shopify.com/static/online-store/theme-skeleton.zip', + role: 'UNPUBLISHED', + }, + '2025-04', + ) expect(theme).not.toBeNull() expect(theme!.id).toEqual(id) expect(theme!.name).toEqual(name) @@ -189,34 +196,33 @@ describe('createTheme', () => { expect(theme!.processing).toBeFalsy() }) - test('does not upload minimum theme assets when src is provided', async () => { + test('does not use skeletonThemeCdn when src is provided', async () => { // Given - vi.mocked(restRequest) - .mockResolvedValueOnce({ - json: {theme: {id, name, role, processing}}, - status: 200, - headers: {}, - }) - .mockResolvedValueOnce({ - json: { - results: [], + vi.mocked(adminRequestDoc).mockResolvedValueOnce({ + themeCreate: { + theme: { + id: `gid://shopify/OnlineStoreTheme/${id}`, + name, + role, }, - status: 207, - headers: {}, - }) + userErrors: [], + }, + }) // When - const theme = await createTheme({...params, src: 'https://example.com/theme.zip'}, session) + const theme = await themeCreate({...params, src: 'https://example.com/theme.zip'}, session) // Then - expect(restRequest).toHaveBeenCalledWith( - 'POST', - '/themes', + expect(adminRequestDoc).toHaveBeenCalledWith( + ThemeCreate, session, - {theme: {...params, src: 'https://example.com/theme.zip'}}, - {}, + { + name: params.name, + source: 'https://example.com/theme.zip', + role: 'UNPUBLISHED', + }, + '2025-04', ) - expect(restRequest).not.toHaveBeenCalledWith('PUT', `/themes/${id}/assets/bulk`, session, undefined, {}) }) }) diff --git a/packages/cli-kit/src/public/node/themes/api.ts b/packages/cli-kit/src/public/node/themes/api.ts index b54eb4a30bf..f4f571f1006 100644 --- a/packages/cli-kit/src/public/node/themes/api.ts +++ b/packages/cli-kit/src/public/node/themes/api.ts @@ -1,9 +1,8 @@ -import {storeAdminUrl} from './urls.js' -import {composeThemeGid, parseGid} from './utils.js' -import * as throttler from '../api/rest-api-throttler.js' +import {composeThemeGid, parseGid, DEVELOPMENT_THEME_ROLE} from './utils.js' import {ThemeUpdate} from '../../../cli/api/graphql/admin/generated/theme_update.js' import {ThemeDelete} from '../../../cli/api/graphql/admin/generated/theme_delete.js' import {ThemePublish} from '../../../cli/api/graphql/admin/generated/theme_publish.js' +import {ThemeCreate} from '../../../cli/api/graphql/admin/generated/theme_create.js' import {GetThemeFileBodies} from '../../../cli/api/graphql/admin/generated/get_theme_file_bodies.js' import {GetThemeFileChecksums} from '../../../cli/api/graphql/admin/generated/get_theme_file_checksums.js' import { @@ -15,21 +14,22 @@ import { OnlineStoreThemeFileBodyInputType, OnlineStoreThemeFilesUpsertFileInput, MetafieldOwnerType, + ThemeRole, } from '../../../cli/api/graphql/admin/generated/types.js' import {MetafieldDefinitionsByOwnerType} from '../../../cli/api/graphql/admin/generated/metafield_definitions_by_owner_type.js' import {GetThemes} from '../../../cli/api/graphql/admin/generated/get_themes.js' import {GetTheme} from '../../../cli/api/graphql/admin/generated/get_theme.js' import {OnlineStorePasswordProtection} from '../../../cli/api/graphql/admin/generated/online_store_password_protection.js' -import {restRequest, RestResponse, adminRequestDoc} from '@shopify/cli-kit/node/api/admin' +import {adminRequestDoc} from '@shopify/cli-kit/node/api/admin' import {AdminSession} from '@shopify/cli-kit/node/session' import {AbortError} from '@shopify/cli-kit/node/error' import {buildTheme} from '@shopify/cli-kit/node/themes/factories' import {Result, Checksum, Key, Theme, ThemeAsset, Operation} from '@shopify/cli-kit/node/themes/types' import {outputDebug} from '@shopify/cli-kit/node/output' -import {sleep} from '@shopify/cli-kit/node/system' export type ThemeParams = Partial> export type AssetParams = Pick & Partial> +const SkeletonThemeCdn = 'https://cdn.shopify.com/static/online-store/theme-skeleton.zip' export async function fetchTheme(id: number, session: AdminSession): Promise { const gid = composeThemeGid(id) @@ -91,20 +91,38 @@ export async function fetchThemes(session: AdminSession): Promise { } } -export async function createTheme(params: ThemeParams, session: AdminSession): Promise { - const response = await request('POST', '/themes', session, {theme: {...params}}) +export async function themeCreate(params: ThemeParams, session: AdminSession): Promise { + const themeSource = params.src ?? SkeletonThemeCdn + const {themeCreate} = await adminRequestDoc( + ThemeCreate, + session, + { + name: params.name ?? '', + source: themeSource, + role: (params.role ?? DEVELOPMENT_THEME_ROLE).toUpperCase() as ThemeRole, + }, + '2025-04', + ) + + if (!themeCreate) { + unexpectedGraphQLError('Failed to create theme') + } - if (!params.src) { - const minimumThemeAssets = [ - {key: 'config/settings_schema.json', value: '[]'}, - {key: 'layout/password.liquid', value: '{{ content_for_header }}{{ content_for_layout }}'}, - {key: 'layout/theme.liquid', value: '{{ content_for_header }}{{ content_for_layout }}'}, - ] + const {theme, userErrors} = themeCreate + if (userErrors.length) { + const userErrors = themeCreate.userErrors.map((error) => error.message).join(', ') + throw new AbortError(userErrors) + } - await bulkUploadThemeAssets(response.json.theme.id, minimumThemeAssets, session) + if (!theme) { + unexpectedGraphQLError('Failed to create theme') } - return buildTheme({...response.json.theme, createdAtRuntime: true}) + return buildTheme({ + id: parseGid(theme.id), + name: theme.name, + role: theme.role.toLowerCase(), + }) } export async function fetchThemeAssets(id: number, filenames: Key[], session: AdminSession): Promise { @@ -406,135 +424,10 @@ export async function passwordProtected(session: AdminSession): Promise return passwordProtection.enabled } -async function request( - method: string, - path: string, - session: AdminSession, - params?: T, - searchParams: {[name: string]: string} = {}, - retries = 1, -): Promise { - const response = await throttler.throttle(() => restRequest(method, path, session, params, searchParams)) - - const status = response.status - - throttler.updateApiCallLimitFromResponse(response) - - switch (true) { - case status >= 200 && status <= 399: - // Returns the successful reponse - return response - case status === 404: - // Defer the decision when a resource is not found - return response - case status === 429: - // Retry following the "retry-after" header - return throttler.delayAwareRetry(response, () => request(method, path, session, params, searchParams)) - case status === 403: - return handleForbiddenError(response, session) - case status === 401: - /** - * We need to resolve the call to the refresh function at runtime to - * avoid a circular reference. - * - * This won't be necessary when https://github.com/Shopify/cli/issues/4769 - * gets resolved, and this condition must be removed then. - */ - if ('refresh' in session) { - const refresh = session.refresh as () => Promise - await refresh() - } - - // Retry 401 errors to be resilient to authentication errors. - return handleRetriableError({ - path, - retries, - retry: () => { - return request(method, path, session, params, searchParams, retries + 1) - }, - fail: () => { - throw new AbortError(`[${status}] API request unauthorized error`) - }, - }) - case status === 422: - throw new AbortError(`[${status}] API request unprocessable content: ${errors(response)}`) - case status >= 400 && status <= 499: - throw new AbortError(`[${status}] API request client error`) - case status >= 500 && status <= 599: - // Retry 500-family of errors as that may solve the issue (especially in 503 errors) - return handleRetriableError({ - path, - retries, - retry: () => { - return request(method, path, session, params, searchParams, retries + 1) - }, - fail: () => { - throw new AbortError(`[${status}] API request server error`) - }, - }) - default: - throw new AbortError(`[${status}] API request unexpected error`) - } -} - -function handleForbiddenError(response: RestResponse, session: AdminSession): never { - const store = session.storeFqdn - const adminUrl = storeAdminUrl(session) - const error = errorMessage(response) - - if (error.match(/Cannot delete generated asset/) !== null) { - throw new AbortError(error) - } - - throw new AbortError( - `You are not authorized to edit themes on "${store}".`, - "You can't use Shopify CLI with development stores if you only have Partner staff " + - 'member access. If you want to use Shopify CLI to work on a development store, then ' + - 'you should be the store owner or create a staff account on the store.' + - '\n\n' + - "If you're the store owner, then you need to log in to the store directly using the " + - `store URL at least once (for example, using "${adminUrl}") before you log in using ` + - 'Shopify CLI. Logging in to the Shopify admin directly connects the development ' + - 'store with your Shopify login.', - ) -} - -function errors(response: RestResponse) { - return JSON.stringify(response.json?.errors) -} - -function errorMessage(response: RestResponse): string { - const message = response.json?.message - - if (typeof message === 'string') { - return message - } - - return '' -} - function unexpectedGraphQLError(message: string): never { throw new AbortError(message) } -interface RetriableErrorOptions { - path: string - retries: number - retry: () => Promise - fail: () => never -} - -async function handleRetriableError({path, retries, retry, fail}: RetriableErrorOptions): Promise { - if (retries >= 3) { - fail() - } - - outputDebug(`[${retries}] Retrying '${path}' request...`) - - await sleep(0.2) - return retry() -} - function themeGid(id: number): string { return `gid://shopify/OnlineStoreTheme/${id}` } diff --git a/packages/cli-kit/src/public/node/themes/theme-manager.ts b/packages/cli-kit/src/public/node/themes/theme-manager.ts index 564575b7b50..fe7cdb5a9d7 100644 --- a/packages/cli-kit/src/public/node/themes/theme-manager.ts +++ b/packages/cli-kit/src/public/node/themes/theme-manager.ts @@ -1,4 +1,4 @@ -import {fetchTheme, createTheme} from './api.js' +import {fetchTheme, themeCreate} from './api.js' import {generateThemeName} from '../../../private/node/themes/generate-theme-name.js' import {AdminSession} from '@shopify/cli-kit/node/session' import {BugError} from '@shopify/cli-kit/node/error' @@ -37,9 +37,9 @@ export abstract class ThemeManager { } async create(themeRole?: Role, themeName?: string) { - const name = themeName || generateThemeName(this.context) - const role = themeRole || DEVELOPMENT_THEME_ROLE - const theme = await createTheme( + const name = themeName ?? generateThemeName(this.context) + const role = themeRole ?? DEVELOPMENT_THEME_ROLE + const theme = await themeCreate( { name, role, diff --git a/packages/theme/src/cli/services/push.test.ts b/packages/theme/src/cli/services/push.test.ts index e83921bfc0a..c3be82342e9 100644 --- a/packages/theme/src/cli/services/push.test.ts +++ b/packages/theme/src/cli/services/push.test.ts @@ -7,7 +7,7 @@ import {findOrSelectTheme} from '../utilities/theme-selector.js' import {runThemeCheck} from '../commands/theme/check.js' import {buildTheme} from '@shopify/cli-kit/node/themes/factories' import {test, describe, vi, expect, beforeEach} from 'vitest' -import {createTheme, fetchTheme, themePublish} from '@shopify/cli-kit/node/themes/api' +import {themeCreate, fetchTheme, themePublish} from '@shopify/cli-kit/node/themes/api' import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' import { DEVELOPMENT_THEME_ROLE, @@ -191,7 +191,7 @@ describe('push', () => { 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(themeCreate).mockResolvedValue(buildTheme({id: 2, name: 'Theme', role: UNPUBLISHED_THEME_ROLE})) vi.mocked(fetchTheme).mockResolvedValue(undefined) const flags: PushFlags = {unpublished: true} @@ -206,7 +206,7 @@ describe('createOrSelectTheme', async () => { 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(themeCreate).mockResolvedValue(buildTheme({id: 1, name: 'Theme', role: DEVELOPMENT_THEME_ROLE})) vi.mocked(fetchTheme).mockResolvedValue(undefined) const flags: PushFlags = {development: true} @@ -220,7 +220,7 @@ describe('createOrSelectTheme', async () => { 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(themeCreate).mockResolvedValue(buildTheme({id: 1, name: 'Theme', role: DEVELOPMENT_THEME_ROLE})) vi.mocked(fetchTheme).mockResolvedValue(undefined) const flags: PushFlags = {development: true, unpublished: true} diff --git a/packages/theme/src/cli/services/push.ts b/packages/theme/src/cli/services/push.ts index 5a44dde6f47..092aabfdf60 100644 --- a/packages/theme/src/cli/services/push.ts +++ b/packages/theme/src/cli/services/push.ts @@ -9,7 +9,7 @@ import {Role} from '../utilities/theme-selector/fetch.js' import {configureCLIEnvironment} from '../utilities/cli-config.js' import {runThemeCheck} from '../commands/theme/check.js' import {AdminSession, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' -import {createTheme, fetchChecksums, themePublish} from '@shopify/cli-kit/node/themes/api' +import {themeCreate, fetchChecksums, themePublish} 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 { @@ -306,7 +306,7 @@ export async function createOrSelectTheme(adminSession: AdminSession, flags: Pus return themeManager.findOrCreate() } else if (unpublished) { const themeName = theme ?? (await promptThemeName('Name of the new theme')) - return createTheme( + return themeCreate( { name: themeName, role: UNPUBLISHED_THEME_ROLE, diff --git a/packages/theme/src/cli/utilities/development-theme-manager.test.ts b/packages/theme/src/cli/utilities/development-theme-manager.test.ts index 4fbc5279f08..3914b350491 100644 --- a/packages/theme/src/cli/utilities/development-theme-manager.test.ts +++ b/packages/theme/src/cli/utilities/development-theme-manager.test.ts @@ -4,7 +4,7 @@ import { DEVELOPMENT_THEME_NOT_FOUND, } from './development-theme-manager.js' import {getDevelopmentTheme, setDevelopmentTheme, removeDevelopmentTheme} from '../services/local-storage.js' -import {createTheme, fetchTheme} from '@shopify/cli-kit/node/themes/api' +import {themeCreate, fetchTheme} from '@shopify/cli-kit/node/themes/api' import {buildTheme} from '@shopify/cli-kit/node/themes/factories' import {beforeEach, describe, expect, vi, test} from 'vitest' import {Theme} from '@shopify/cli-kit/node/themes/types' @@ -31,7 +31,7 @@ describe('DevelopmentThemeManager', () => { vi.mocked(removeDevelopmentTheme).mockImplementation(() => undefined) vi.mocked(fetchTheme).mockImplementation((id: number) => Promise.resolve(themeTestDatabase[id])) - vi.mocked(createTheme).mockImplementation(({name, role}) => + vi.mocked(themeCreate).mockImplementation(({name, role}) => Promise.resolve( buildTheme({ id: newThemeId, diff --git a/packages/theme/src/cli/utilities/repl/repl-theme-manager.test.ts b/packages/theme/src/cli/utilities/repl/repl-theme-manager.test.ts index a3e74cf0ee3..8c9b2594b38 100644 --- a/packages/theme/src/cli/utilities/repl/repl-theme-manager.test.ts +++ b/packages/theme/src/cli/utilities/repl/repl-theme-manager.test.ts @@ -3,7 +3,7 @@ import {setREPLTheme, removeREPLTheme, getREPLTheme, getDevelopmentTheme} from ' import {AdminSession} from '@shopify/cli-kit/node/session' import {beforeEach, describe, expect, test, vi} from 'vitest' import {DEVELOPMENT_THEME_ROLE} from '@shopify/cli-kit/node/themes/utils' -import {bulkUploadThemeAssets, createTheme, fetchTheme} from '@shopify/cli-kit/node/themes/api' +import {bulkUploadThemeAssets, themeCreate, fetchTheme} from '@shopify/cli-kit/node/themes/api' vi.mock('@shopify/cli-kit/node/themes/api') vi.mock('../../services/local-storage') @@ -27,7 +27,7 @@ describe('REPLThemeManager', () => { processing: true, createdAtRuntime: true, } - vi.mocked(createTheme).mockResolvedValue(theme) + vi.mocked(themeCreate).mockResolvedValue(theme) // When await themeManager.create(DEVELOPMENT_THEME_ROLE, 'Liquid Console (3.60)') @@ -40,7 +40,7 @@ describe('REPLThemeManager', () => { test('should set the REPL theme in local storage', async () => { // Given const themeName = 'Liquid Console (3.60)' - vi.mocked(createTheme).mockResolvedValue({ + vi.mocked(themeCreate).mockResolvedValue({ id: 123, name: themeName, role: DEVELOPMENT_THEME_ROLE, diff --git a/packages/theme/src/cli/utilities/theme-selector.test.ts b/packages/theme/src/cli/utilities/theme-selector.test.ts index 3f762f9f019..d2a878b73ba 100644 --- a/packages/theme/src/cli/utilities/theme-selector.test.ts +++ b/packages/theme/src/cli/utilities/theme-selector.test.ts @@ -5,7 +5,7 @@ import {test, describe, vi, expect} from 'vitest' import {renderAutocompletePrompt} from '@shopify/cli-kit/node/ui' import {Theme} from '@shopify/cli-kit/node/themes/types' import {promptThemeName} from '@shopify/cli-kit/node/themes/utils' -import {createTheme} from '@shopify/cli-kit/node/themes/api' +import {themeCreate} from '@shopify/cli-kit/node/themes/api' vi.mock('./theme-selector/fetch.js') vi.mock('../services/local-storage.js') @@ -91,7 +91,7 @@ describe('findOrSelectTheme', () => { vi.mocked(renderAutocompletePrompt).mockResolvedValue(newThemeOption(session).value) vi.mocked(promptThemeName).mockResolvedValue(expectedThemeName) - vi.mocked(createTheme).mockResolvedValue(expectedTheme) + vi.mocked(themeCreate).mockResolvedValue(expectedTheme) vi.mocked(fetchStoreThemes).mockResolvedValue(storeThemes) // When @@ -102,7 +102,7 @@ describe('findOrSelectTheme', () => { }) // Then - expect(createTheme).toBeCalledWith({name: 'my new theme', role: 'unpublished'}, session) + expect(themeCreate).toBeCalledWith({name: 'my new theme', role: 'unpublished'}, session) expect(actualTheme).toBe(expectedTheme) }) }) diff --git a/packages/theme/src/cli/utilities/theme-selector.ts b/packages/theme/src/cli/utilities/theme-selector.ts index a5cb76f97f5..0b37743f166 100644 --- a/packages/theme/src/cli/utilities/theme-selector.ts +++ b/packages/theme/src/cli/utilities/theme-selector.ts @@ -4,7 +4,7 @@ import {getDevelopmentTheme} from '../services/local-storage.js' import {renderAutocompletePrompt} from '@shopify/cli-kit/node/ui' import {AdminSession} from '@shopify/cli-kit/node/session' import {capitalize} from '@shopify/cli-kit/common/string' -import {createTheme} from '@shopify/cli-kit/node/themes/api' +import {themeCreate} from '@shopify/cli-kit/node/themes/api' import {promptThemeName, UNPUBLISHED_THEME_ROLE} from '@shopify/cli-kit/node/themes/utils' import {AbortError} from '@shopify/cli-kit/node/error' import {Theme} from '@shopify/cli-kit/node/themes/types' @@ -97,7 +97,7 @@ export function newThemeOption(session: AdminSession): { value: async () => { const role = UNPUBLISHED_THEME_ROLE const name = await promptThemeName('Name of the new theme') - const theme = await createTheme({name, role}, session) + const theme = await themeCreate({name, role}, session) if (!theme) { throw new AbortError('The theme could not be created.')