diff --git a/packages/theme/src/cli/utilities/theme-environment/hot-reload/error-overlay.ts b/packages/theme/src/cli/utilities/theme-environment/hot-reload/error-overlay.ts new file mode 100644 index 00000000000..6482ee6b4df --- /dev/null +++ b/packages/theme/src/cli/utilities/theme-environment/hot-reload/error-overlay.ts @@ -0,0 +1,74 @@ +const OVERLAY_STYLES = { + container: ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + `, + dialog: ` + background: rgba(200, 200, 200, 0.9); + backdrop-filter: blur(10px); + border-radius: 10px; + padding: 20px; + font-family: system-ui, -apple-system, sans-serif; + max-width: 80%; + max-height: 80%; + box-shadow: 0px 0px 10px rgba(0,0,0,0.5); + position: relative; + overflow-y: auto; + `, + closeButton: ` + position: absolute; + top: 10px; + right: 10px; + background: transparent; + border: none; + font-size: 16px; + cursor: pointer; + `, + errorItem: ` + margin-bottom: 16px; + text-align: left; + `, + errorMessage: ` + margin: 8px 0; + white-space: normal; + word-wrap: break-word; + `, +} + +export function getErrorOverlay(errors: Map): string { + const errorContent = Array.from(errors.entries()) + .map( + ([fileKey, messages]) => ` +
+ ${fileKey} + ${messages.map((msg) => `
- ${msg}
`).join('')} +
+ `, + ) + .join('') + + return ` +
+
+ ${errorContent} + +
+
+ ` +} + +export function injectErrorIntoHtml(html: string, errors: Map): string { + return html + getErrorOverlay(errors) +} diff --git a/packages/theme/src/cli/utilities/theme-environment/html.test.ts b/packages/theme/src/cli/utilities/theme-environment/html.test.ts new file mode 100644 index 00000000000..d773f5a7a29 --- /dev/null +++ b/packages/theme/src/cli/utilities/theme-environment/html.test.ts @@ -0,0 +1,40 @@ +import {getErrorOverlay, injectErrorIntoHtml} from './hot-reload/error-overlay.js' +import {describe, expect, test} from 'vitest' + +describe('html', () => { + test('injects error overlay into HTML with body tag', () => { + // Given + const html = '
content
' + const errors = new Map([ + ['assets/theme.css', ['Syntax error in line 10']], + ['sections/header.liquid', ['Missing end tag', 'Invalid liquid syntax']], + ]) + + // When + const result = injectErrorIntoHtml(html, errors) + + // Then + const overlay = getErrorOverlay(errors) + expect(result).toBe(html + overlay) + expect(result).toContain('assets/theme.css') + expect(result).toContain('Syntax error in line 10') + expect(result).toContain('sections/header.liquid') + expect(result).toContain('Missing end tag') + expect(result).toContain('Invalid liquid syntax') + }) + + test('preserves original HTML content', () => { + // Given + const html = 'My Store
content
' + const errors = new Map([['file.css', ['Error']]]) + + // When + const result = injectErrorIntoHtml(html, errors) + + // Then + const overlay = getErrorOverlay(errors) + expect(result).toBe(html + overlay) + expect(result).toContain('My Store') + expect(result).toContain('
content
') + }) +}) diff --git a/packages/theme/src/cli/utilities/theme-environment/html.ts b/packages/theme/src/cli/utilities/theme-environment/html.ts index 8e6097da653..ebc3e39c4fe 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 {injectErrorIntoHtml} from './hot-reload/error-overlay.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' @@ -35,6 +36,10 @@ export function getHtmlHandler(theme: Theme, ctx: DevServerContext) { html = injectHotReloadScript(html) } + if (ctx.localThemeFileSystem.uploadErrors.size > 0) { + html = injectErrorIntoHtml(html, ctx.localThemeFileSystem.uploadErrors) + } + return html }) .catch(async (error: H3Error<{requestId?: string; url?: string}>) => {