@@ -5,16 +5,19 @@ import {getProxyHandler} from './proxy.js'
55import { reconcileAndPollThemeEditorChanges } from './remote-theme-watcher.js'
66import { uploadTheme } from '../theme-uploader.js'
77import { renderTasksToStdErr } from '../theme-ui.js'
8- import { createAbortCatchError } from '../errors.js'
8+ import { renderThrownError } from '../errors.js'
9+ import { promiseWithResolvers } from '../../polyfills/promiseWithResolvers.js'
910import { createApp , defineEventHandler , defineLazyEventHandler , toNodeListener , handleCors } from 'h3'
1011import { fetchChecksums } from '@shopify/cli-kit/node/themes/api'
1112import { createServer } from 'node:http'
1213import type { Checksum , Theme } from '@shopify/cli-kit/node/themes/types'
1314import type { DevServerContext } from './types.js'
1415
1516export function setupDevServer ( theme : Theme , ctx : DevServerContext ) {
17+ const { promise : backgroundJobPromise , reject : rejectBackgroundJob } = promiseWithResolvers < never > ( )
18+
1619 const watcherPromise = setupInMemoryTemplateWatcher ( theme , ctx )
17- const envSetup = ensureThemeEnvironmentSetup ( theme , ctx )
20+ const envSetup = ensureThemeEnvironmentSetup ( theme , ctx , rejectBackgroundJob )
1821 const workPromise = Promise . all ( [ watcherPromise , envSetup . workPromise ] ) . then ( ( ) =>
1922 ctx . localThemeFileSystem . startWatcher ( theme . id . toString ( ) , ctx . session ) ,
2023 )
@@ -25,31 +28,43 @@ export function setupDevServer(theme: Theme, ctx: DevServerContext) {
2528 serverStart : server . start ,
2629 dispatchEvent : server . dispatch ,
2730 renderDevSetupProgress : envSetup . renderProgress ,
31+ backgroundJobPromise,
2832 }
2933}
3034
31- function ensureThemeEnvironmentSetup ( theme : Theme , ctx : DevServerContext ) {
32- const abort = createAbortCatchError ( 'Failed to perform the initial theme synchronization.' )
35+ function ensureThemeEnvironmentSetup (
36+ theme : Theme ,
37+ ctx : DevServerContext ,
38+ rejectBackgroundJob : ( reason ?: unknown ) => void ,
39+ ) {
40+ const abort = ( error : Error ) : never => {
41+ renderThrownError ( 'Failed to perform the initial theme synchronization.' , error )
42+ rejectBackgroundJob ( error )
43+ // Return a never-resolving promise to stop this promise chain without throwing.
44+ // Throwing would trigger catch handlers and continue execution. This stops the
45+ // chain while the error is handled through the separate backgroundJobPromise channel.
46+ return new Promise < never > ( ( ) => { } ) as never
47+ }
3348
34- const remoteChecksumsPromise = fetchChecksums ( theme . id , ctx . session ) . catch ( abort )
49+ const remoteChecksumsPromise = fetchChecksums ( theme . id , ctx . session )
3550
36- const reconcilePromise = remoteChecksumsPromise
37- . then ( ( remoteChecksums ) => handleThemeEditorSync ( theme , ctx , remoteChecksums ) )
38- . catch ( abort )
51+ const reconcilePromise = remoteChecksumsPromise . then ( ( remoteChecksums ) =>
52+ handleThemeEditorSync ( theme , ctx , remoteChecksums , rejectBackgroundJob ) ,
53+ )
3954
40- const uploadPromise = reconcilePromise
41- . then ( async ( { updatedRemoteChecksumsPromise} ) => {
42- const updatedRemoteChecksums = await updatedRemoteChecksumsPromise
43- return uploadTheme ( theme , ctx . session , updatedRemoteChecksums , ctx . localThemeFileSystem , {
44- nodelete : ctx . options . noDelete ,
45- deferPartialWork : true ,
46- backgroundWorkCatch : abort ,
47- } )
55+ const uploadPromise = reconcilePromise . then ( async ( { updatedRemoteChecksumsPromise} ) => {
56+ const updatedRemoteChecksums = await updatedRemoteChecksumsPromise
57+ return uploadTheme ( theme , ctx . session , updatedRemoteChecksums , ctx . localThemeFileSystem , {
58+ nodelete : ctx . options . noDelete ,
59+ deferPartialWork : true ,
60+ backgroundWorkCatch : abort ,
4861 } )
49- . catch ( abort )
62+ } )
63+
64+ const workPromise = uploadPromise . then ( ( result ) => result . workPromise ) . catch ( abort )
5065
5166 return {
52- workPromise : uploadPromise . then ( ( result ) => result . workPromise ) . catch ( abort ) ,
67+ workPromise,
5368 renderProgress : async ( ) => {
5469 if ( ctx . options . themeEditorSync ) {
5570 const { workPromise} = await reconcilePromise
@@ -74,16 +89,24 @@ function handleThemeEditorSync(
7489 theme : Theme ,
7590 ctx : DevServerContext ,
7691 remoteChecksums : Checksum [ ] ,
92+ rejectBackgroundJob : ( reason ?: unknown ) => void ,
7793) : Promise < {
7894 updatedRemoteChecksumsPromise : Promise < Checksum [ ] >
7995 workPromise : Promise < void >
8096} > {
8197 if ( ctx . options . themeEditorSync ) {
82- return reconcileAndPollThemeEditorChanges ( theme , ctx . session , remoteChecksums , ctx . localThemeFileSystem , {
83- noDelete : ctx . options . noDelete ,
84- ignore : ctx . options . ignore ,
85- only : ctx . options . only ,
86- } )
98+ return reconcileAndPollThemeEditorChanges (
99+ theme ,
100+ ctx . session ,
101+ remoteChecksums ,
102+ ctx . localThemeFileSystem ,
103+ {
104+ noDelete : ctx . options . noDelete ,
105+ ignore : ctx . options . ignore ,
106+ only : ctx . options . only ,
107+ } ,
108+ rejectBackgroundJob ,
109+ )
87110 } else {
88111 return Promise . resolve ( {
89112 updatedRemoteChecksumsPromise : Promise . resolve ( remoteChecksums ) ,
0 commit comments