@shopify/cli-kit
Version:
A set of utilities, interfaces, and models that are common across all the platform features
472 lines • 17.1 kB
JavaScript
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