UNPKG

@shopify/cli-kit

Version:

A set of utilities, interfaces, and models that are common across all the platform features

472 lines • 17.1 kB
import { composeThemeGid, parseGid, DEVELOPMENT_THEME_ROLE } from './utils.js'; import { buildTheme } from './factories.js'; import { Operation } from './types.js'; import { ThemeUpdate } from '../../../cli/api/graphql/admin/generated/theme_update.js'; import { ThemeDelete } from '../../../cli/api/graphql/admin/generated/theme_delete.js'; import { ThemeDuplicate } from '../../../cli/api/graphql/admin/generated/theme_duplicate.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 { ThemeFilesUpsert, } from '../../../cli/api/graphql/admin/generated/theme_files_upsert.js'; import { ThemeFilesDelete } from '../../../cli/api/graphql/admin/generated/theme_files_delete.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 { adminRequestDoc } from '../api/admin.js'; import { AbortError } from '../error.js'; import { outputDebug } from '../output.js'; const SkeletonThemeCdn = 'https://cdn.shopify.com/static/online-store/theme-skeleton.zip'; const THEME_API_NETWORK_BEHAVIOUR = { useNetworkLevelRetry: true, useAbortSignal: false, maxRetryTimeMs: 90 * 1000, }; export async function fetchTheme(id, session) { const gid = composeThemeGid(id); try { const { theme } = await adminRequestDoc({ query: GetTheme, session, variables: { id: gid }, responseOptions: { handleErrors: false }, requestBehaviour: THEME_API_NETWORK_BEHAVIOUR, }); if (theme) { return buildTheme({ id: parseGid(theme.id), processing: theme.processing, role: theme.role.toLowerCase(), name: theme.name, }); } // eslint-disable-next-line no-catch-all/no-catch-all } catch (_error) { /** * Consumers of this and other theme APIs in this file expect either a theme * or `undefined`. * * Error handlers should not inspect GraphQL error messages directly, as * they are internationalized. */ outputDebug(`Error fetching theme with ID: ${id}`); } } export async function fetchThemes(session) { const themes = []; let after = null; while (true) { // eslint-disable-next-line no-await-in-loop const response = await adminRequestDoc({ query: GetThemes, session, variables: { after }, responseOptions: { handleErrors: false }, requestBehaviour: THEME_API_NETWORK_BEHAVIOUR, }); if (!response.themes) { unexpectedGraphQLError('Failed to fetch themes'); } const { nodes, pageInfo } = response.themes; nodes.forEach((theme) => { const t = buildTheme({ id: parseGid(theme.id), processing: theme.processing, role: theme.role.toLowerCase(), name: theme.name, }); if (t) { themes.push(t); } }); if (!pageInfo.hasNextPage) { return themes; } after = pageInfo.endCursor; } } export async function themeCreate(params, session) { const themeSource = params.src ?? SkeletonThemeCdn; const { themeCreate } = await adminRequestDoc({ query: ThemeCreate, session, variables: { name: params.name ?? '', source: themeSource, role: (params.role ?? DEVELOPMENT_THEME_ROLE).toUpperCase(), }, responseOptions: { handleErrors: false }, requestBehaviour: THEME_API_NETWORK_BEHAVIOUR, }); if (!themeCreate) { unexpectedGraphQLError('Failed to create theme'); } const { theme, userErrors } = themeCreate; if (userErrors.length) { const userErrors = themeCreate.userErrors.map((error) => error.message).join(', '); throw new AbortError(userErrors); } if (!theme) { unexpectedGraphQLError('Failed to create theme'); } return buildTheme({ id: parseGid(theme.id), name: theme.name, role: theme.role.toLowerCase(), }); } export async function fetchThemeAssets(id, filenames, session) { const assets = []; let after = null; while (true) { // eslint-disable-next-line no-await-in-loop const response = await adminRequestDoc({ query: GetThemeFileBodies, session, variables: { id: themeGid(id), filenames, after }, responseOptions: { handleErrors: false }, requestBehaviour: THEME_API_NETWORK_BEHAVIOUR, }); if (!response.theme?.files?.nodes || !response.theme?.files?.pageInfo) { const userErrors = response.theme?.files?.userErrors.map((error) => error.filename).join(', '); unexpectedGraphQLError(`Error fetching assets: ${userErrors}`); } const { nodes, pageInfo } = response.theme.files; assets.push( // eslint-disable-next-line no-await-in-loop ...(await Promise.all(nodes.map(async (file) => { const { attachment, value } = await parseThemeFileContent(file.body); return { attachment, key: file.filename, checksum: file.checksumMd5, value, }; })))); if (!pageInfo.hasNextPage) { return assets; } after = pageInfo.endCursor; } } export async function deleteThemeAssets(id, filenames, session) { const batchSize = 50; const results = []; for (let i = 0; i < filenames.length; i += batchSize) { const batch = filenames.slice(i, i + batchSize); // eslint-disable-next-line no-await-in-loop const { themeFilesDelete } = await adminRequestDoc({ query: ThemeFilesDelete, session, variables: { themeId: composeThemeGid(id), files: batch, }, requestBehaviour: THEME_API_NETWORK_BEHAVIOUR, }); if (!themeFilesDelete) { unexpectedGraphQLError('Failed to delete theme assets'); } const { deletedThemeFiles, userErrors } = themeFilesDelete; if (deletedThemeFiles) { deletedThemeFiles.forEach((file) => { results.push({ key: file.filename, success: true, operation: Operation.Delete }); }); } if (userErrors.length > 0) { userErrors.forEach((error) => { if (error.filename) { results.push({ key: error.filename, success: false, operation: Operation.Delete, errors: { asset: [error.message] }, }); } else { unexpectedGraphQLError(`Failed to delete theme assets: ${error.message}`); } }); } } return results; } export async function bulkUploadThemeAssets(id, assets, session) { const results = []; for (let i = 0; i < assets.length; i += 50) { const chunk = assets.slice(i, i + 50); const files = prepareFilesForUpload(chunk); // eslint-disable-next-line no-await-in-loop const uploadResults = await uploadFiles(id, files, session); results.push(...processUploadResults(uploadResults)); } return results; } function prepareFilesForUpload(assets) { return assets.map((asset) => { if (asset.attachment) { return { filename: asset.key, body: { type: 'BASE64', value: asset.attachment, }, }; } else { return { filename: asset.key, body: { type: 'TEXT', value: asset.value ?? '', }, }; } }); } async function uploadFiles(themeId, files, session) { return adminRequestDoc({ query: ThemeFilesUpsert, session, variables: { themeId: themeGid(themeId), files }, requestBehaviour: THEME_API_NETWORK_BEHAVIOUR, }); } function processUploadResults(uploadResults) { const { themeFilesUpsert } = uploadResults; if (!themeFilesUpsert) { unexpectedGraphQLError('Failed to upload theme files'); } const { upsertedThemeFiles, userErrors } = themeFilesUpsert; const results = []; upsertedThemeFiles?.forEach((file) => { results.push({ key: file.filename, success: true, operation: Operation.Upload, }); }); userErrors.forEach((error) => { if (!error.filename) { unexpectedGraphQLError(`Error uploading theme files: ${error.message}`); } results.push({ key: error.filename, success: false, operation: Operation.Upload, errors: { asset: [error.message] }, }); }); return results; } export async function fetchChecksums(id, session) { const checksums = []; let after = null; while (true) { // eslint-disable-next-line no-await-in-loop const response = await adminRequestDoc({ query: GetThemeFileChecksums, session, variables: { id: themeGid(id), after }, responseOptions: { handleErrors: false }, requestBehaviour: THEME_API_NETWORK_BEHAVIOUR, }); if (!response?.theme?.files?.nodes || !response?.theme?.files?.pageInfo) { const userErrors = response.theme?.files?.userErrors.map((error) => error.filename).join(', '); throw new AbortError(`Failed to fetch checksums for: ${userErrors}`); } const { nodes, pageInfo } = response.theme.files; checksums.push(...nodes.map((file) => ({ key: file.filename, checksum: file.checksumMd5, }))); if (!pageInfo.hasNextPage) { return checksums; } after = pageInfo.endCursor; } } export async function themeUpdate(id, params, session) { const name = params.name; const input = {}; if (name) { input.name = name; } const { themeUpdate } = await adminRequestDoc({ query: ThemeUpdate, session, variables: { id: composeThemeGid(id), input }, requestBehaviour: THEME_API_NETWORK_BEHAVIOUR, }); if (!themeUpdate) { // An unexpected error occurred during the GraphQL request execution unexpectedGraphQLError('Failed to update theme'); } const { theme, userErrors } = themeUpdate; if (userErrors.length) { const userErrors = themeUpdate.userErrors.map((error) => error.message).join(', '); throw new AbortError(userErrors); } if (!theme) { // An unexpected error if neither theme nor userErrors are returned unexpectedGraphQLError('Failed to update theme'); } return buildTheme({ id: parseGid(theme.id), name: theme.name, role: theme.role.toLowerCase(), }); } export async function themePublish(id, session) { const { themePublish } = await adminRequestDoc({ query: ThemePublish, session, variables: { id: composeThemeGid(id) }, requestBehaviour: THEME_API_NETWORK_BEHAVIOUR, }); if (!themePublish) { // An unexpected error occurred during the GraphQL request execution unexpectedGraphQLError('Failed to update theme'); } const { theme, userErrors } = themePublish; if (userErrors.length) { const userErrors = themePublish.userErrors.map((error) => error.message).join(', '); throw new AbortError(userErrors); } if (!theme) { // An unexpected error if neither theme nor userErrors are returned unexpectedGraphQLError('Failed to update theme'); } return buildTheme({ id: parseGid(theme.id), name: theme.name, role: theme.role.toLowerCase(), }); } export async function themeDelete(id, session) { const { themeDelete } = await adminRequestDoc({ query: ThemeDelete, session, variables: { id: composeThemeGid(id) }, requestBehaviour: THEME_API_NETWORK_BEHAVIOUR, }); if (!themeDelete) { // An unexpected error occurred during the GraphQL request execution unexpectedGraphQLError('Failed to update theme'); } const { deletedThemeId, userErrors } = themeDelete; if (userErrors.length) { const userErrors = themeDelete.userErrors.map((error) => error.message).join(', '); throw new AbortError(userErrors); } if (!deletedThemeId) { // An unexpected error if neither theme nor userErrors are returned unexpectedGraphQLError('Failed to update theme'); } return true; } export async function themeDuplicate(id, name, session) { let requestId; const { themeDuplicate } = await adminRequestDoc({ query: ThemeDuplicate, session, variables: { id: composeThemeGid(id), name }, requestBehaviour: THEME_API_NETWORK_BEHAVIOUR, version: '2025-10', responseOptions: { onResponse: (response) => { requestId = response.headers.get('x-request-id') ?? undefined; }, }, }); if (!themeDuplicate) { // An unexpected error occurred during the GraphQL request execution return { theme: undefined, userErrors: [{ message: 'Failed to duplicate theme' }], requestId, }; } const { newTheme, userErrors } = themeDuplicate; if (userErrors.length > 0) { return { theme: undefined, userErrors, requestId, }; } if (!newTheme) { // An unexpected error if neither theme nor userErrors are returned return { theme: undefined, userErrors: [{ message: 'Failed to duplicate theme' }], requestId, }; } const theme = buildTheme({ id: parseGid(newTheme.id), name: newTheme.name, role: newTheme.role.toLowerCase(), }); return { theme, userErrors: [], requestId, }; } export async function metafieldDefinitionsByOwnerType(type, session) { const { metafieldDefinitions } = await adminRequestDoc({ query: MetafieldDefinitionsByOwnerType, session, variables: { ownerType: type }, }); return metafieldDefinitions.nodes.map((definition) => ({ key: definition.key, namespace: definition.namespace, name: definition.name, description: definition.description, type: { name: definition.type.name, category: definition.type.category, }, })); } export async function passwordProtected(session) { const { onlineStore } = await adminRequestDoc({ query: OnlineStorePasswordProtection, session, }); if (!onlineStore) { unexpectedGraphQLError("Unable to get details about the storefront's password protection"); } const { passwordProtection } = onlineStore; return passwordProtection.enabled; } function unexpectedGraphQLError(message) { throw new AbortError(message); } function themeGid(id) { return `gid://shopify/OnlineStoreTheme/${id}`; } export async function parseThemeFileContent(body) { switch (body.__typename) { case 'OnlineStoreThemeFileBodyText': return { value: body.content }; case 'OnlineStoreThemeFileBodyBase64': return { attachment: body.contentBase64 }; case 'OnlineStoreThemeFileBodyUrl': try { // eslint-disable-next-line no-restricted-globals const response = await fetch(body.url); const arrayBuffer = await response.arrayBuffer(); return { attachment: Buffer.from(arrayBuffer).toString('base64') }; } catch (error) { // Raise error if we can't download the file throw new AbortError(`Error downloading content from URL: ${body.url}`); } } } //# sourceMappingURL=api.js.map