diff --git a/packages/cli-kit/src/public/node/themes/types.ts b/packages/cli-kit/src/public/node/themes/types.ts index 1fdf1e6d2eb..ef2786c7d0e 100644 --- a/packages/cli-kit/src/public/node/themes/types.ts +++ b/packages/cli-kit/src/public/node/themes/types.ts @@ -99,6 +99,11 @@ export interface ThemeFileSystem extends VirtualFileSystem { * Applies filters to ignore files from .shopifyignore file, --ignore and --only flags. */ applyIgnoreFilters: (files: T[]) => T[] + + /** + * Stores upload errors returned when uploading files via the Asset API + */ + uploadErrors: Map } /** diff --git a/packages/theme/src/cli/utilities/theme-environment/hot-reload/error-page.ts b/packages/theme/src/cli/utilities/theme-environment/hot-reload/error-page.ts new file mode 100644 index 00000000000..d04945c29e2 --- /dev/null +++ b/packages/theme/src/cli/utilities/theme-environment/hot-reload/error-page.ts @@ -0,0 +1,28 @@ +interface Error { + message: string + code: string +} + +export function getErrorPage(options: {title: string; header: string; errors: Error[]}) { + const html = String.raw + + return html` + + ${options.title ?? 'Unknown error'} + + +

${options.header}

+ + ${options.errors + .map( + (error) => + `

${error.message}

+
${error.code}
`, + ) + .join('')} + + ` +} diff --git a/packages/theme/src/cli/utilities/theme-environment/html.ts b/packages/theme/src/cli/utilities/theme-environment/html.ts index 8e6097da653..858f1cb15c3 100644 --- a/packages/theme/src/cli/utilities/theme-environment/html.ts +++ b/packages/theme/src/cli/utilities/theme-environment/html.ts @@ -1,6 +1,7 @@ import {getProxyStorefrontHeaders, patchRenderingResponse} from './proxy.js' import {getInMemoryTemplates, injectHotReloadScript} from './hot-reload/server.js' import {render} from './storefront-renderer.js' +import {getErrorPage} from './hot-reload/error-page.js' import {getExtensionInMemoryTemplates} from '../theme-ext-environment/theme-ext-server.js' import {logRequestLine} from '../log-request-line.js' import {defineEventHandler, getCookie, setResponseHeader, setResponseStatus, type H3Error} from 'h3' @@ -31,6 +32,17 @@ export function getHtmlHandler(theme: Theme, ctx: DevServerContext) { assertThemeId(response, html, String(theme.id)) + if (ctx.localThemeFileSystem.uploadErrors.size > 0) { + html = getErrorPage({ + title: 'Failed to Upload Theme Files', + header: 'Upload Errors', + errors: Array.from(ctx.localThemeFileSystem.uploadErrors.entries()).map(([file, errors]) => ({ + message: file, + code: errors.join('\n'), + })), + }) + } + if (ctx.options.liveReload !== 'off') { html = injectHotReloadScript(html) } @@ -52,8 +64,12 @@ export function getHtmlHandler(theme: Theme, ctx: DevServerContext) { let errorPageHtml = getErrorPage({ title, header: title, - message: [...rest, cause?.message ?? error.message].join('
'), - code: error.stack?.replace(`${error.message}\n`, '') ?? '', + errors: [ + { + message: [...rest, cause?.message ?? error.message].join('
'), + code: error.stack?.replace(`${error.message}\n`, '') ?? '', + }, + ], }) if (ctx.options.liveReload !== 'off') { @@ -65,24 +81,6 @@ export function getHtmlHandler(theme: Theme, ctx: DevServerContext) { }) } -function getErrorPage(options: {title: string; header: string; message: string; code: string}) { - const html = String.raw - - return html` - - ${options.title ?? 'Unknown error'} - - -

${options.header}

-

${options.message}

-
${options.code}
- - ` -} - function assertThemeId(response: Response, html: string, expectedThemeId: string) { /** * DOM example: diff --git a/packages/theme/src/cli/utilities/theme-fs-empty.ts b/packages/theme/src/cli/utilities/theme-fs-empty.ts index 51c40f4520d..126170fb0b3 100644 --- a/packages/theme/src/cli/utilities/theme-fs-empty.ts +++ b/packages/theme/src/cli/utilities/theme-fs-empty.ts @@ -13,6 +13,7 @@ function emptyFileSystem(): T { root: '', files: new Map(), unsyncedFileKeys: new Set(), + uploadErrors: new Map(), ready: () => Promise.resolve(), delete: async (_: string) => {}, read: async (_: string) => '', diff --git a/packages/theme/src/cli/utilities/theme-fs.test.ts b/packages/theme/src/cli/utilities/theme-fs.test.ts index 9b09633dc77..f0a81010ac9 100644 --- a/packages/theme/src/cli/utilities/theme-fs.test.ts +++ b/packages/theme/src/cli/utilities/theme-fs.test.ts @@ -59,6 +59,7 @@ describe('theme-fs', () => { fsEntry({checksum: '64caf742bd427adcf497bffab63df30c', key: 'templates/404.json'}), ]), unsyncedFileKeys: new Set(), + uploadErrors: new Map(), ready: expect.any(Function), delete: expect.any(Function), write: expect.any(Function), @@ -82,6 +83,7 @@ describe('theme-fs', () => { root, files: new Map(), unsyncedFileKeys: new Set(), + uploadErrors: new Map(), ready: expect.any(Function), delete: expect.any(Function), write: expect.any(Function), diff --git a/packages/theme/src/cli/utilities/theme-fs.ts b/packages/theme/src/cli/utilities/theme-fs.ts index 820a8f68724..abfd7a36123 100644 --- a/packages/theme/src/cli/utilities/theme-fs.ts +++ b/packages/theme/src/cli/utilities/theme-fs.ts @@ -45,6 +45,7 @@ const THEME_PARTITION_REGEX = { export function mountThemeFileSystem(root: string, options?: ThemeFileSystemOptions): ThemeFileSystem { const files = new Map() + const uploadErrors = new Map() const unsyncedFileKeys = new Set() const filterPatterns = { ignoreFromFile: [] as string[], @@ -147,12 +148,12 @@ export function mountThemeFileSystem(root: string, options?: ThemeFileSystemOpti const [result] = await bulkUploadThemeAssets(Number(themeId), [{key: fileKey, value: content}], adminSession) - if (!result?.success) { - throw new Error( - result?.errors?.asset - ? `\n\n${result.errors.asset.map((error) => `- ${error}`).join('\n')}` - : 'Response was not successful.', - ) + if (result?.success) { + uploadErrors.delete(fileKey) + } else { + const errors = result?.errors?.asset ?? ['Response was not successful.'] + uploadErrors.set(fileKey, errors) + throw new Error(errors.length === 1 ? errors[0] : errors.join('\n')) } unsyncedFileKeys.delete(fileKey) @@ -223,6 +224,7 @@ export function mountThemeFileSystem(root: string, options?: ThemeFileSystemOpti root, files, unsyncedFileKeys, + uploadErrors, ready: () => themeSetupPromise, delete: async (fileKey: string) => { files.delete(fileKey) diff --git a/packages/theme/src/cli/utilities/theme-fs/theme-fs-mock-factory.ts b/packages/theme/src/cli/utilities/theme-fs/theme-fs-mock-factory.ts index ce559833cb8..e870c8d190c 100644 --- a/packages/theme/src/cli/utilities/theme-fs/theme-fs-mock-factory.ts +++ b/packages/theme/src/cli/utilities/theme-fs/theme-fs-mock-factory.ts @@ -10,6 +10,7 @@ export function fakeThemeFileSystem( root, files, unsyncedFileKeys: new Set(), + uploadErrors: new Map(), ready: () => Promise.resolve(), delete: async (fileKey: string) => { files.delete(fileKey)