Skip to content

Commit 2b7d391

Browse files
committed
fix theme dev so init failures dont call process exit
1 parent f7eb632 commit 2b7d391

File tree

5 files changed

+88
-35
lines changed

5 files changed

+88
-35
lines changed

packages/theme/src/cli/services/dev.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -119,23 +119,7 @@ export async function dev(options: DevOptions) {
119119
},
120120
}
121121

122-
if (options['theme-editor-sync']) {
123-
session.storefrontPassword = await storefrontPasswordPromise
124-
}
125-
126-
const {serverStart, renderDevSetupProgress} = setupDevServer(options.theme, ctx)
127-
128-
if (!options['theme-editor-sync']) {
129-
session.storefrontPassword = await storefrontPasswordPromise
130-
}
131-
132-
await renderDevSetupProgress()
133-
await serverStart()
134-
135-
renderLinks(urls)
136-
if (options.open) {
137-
openURLSafely(urls.local, 'development server')
138-
}
122+
const {serverStart, renderDevSetupProgress, backgroundJobPromise} = setupDevServer(options.theme, ctx)
139123

140124
readline.emitKeypressEvents(process.stdin)
141125
if (process.stdin.isTTY) {
@@ -167,6 +151,18 @@ export async function dev(options: DevOptions) {
167151
break
168152
}
169153
})
154+
155+
await Promise.all([
156+
backgroundJobPromise,
157+
renderDevSetupProgress()
158+
.then(serverStart)
159+
.then(() => {
160+
renderLinks(urls)
161+
if (options.open) {
162+
openURLSafely(urls.local, 'development server')
163+
}
164+
}),
165+
])
170166
}
171167

172168
export function openURLSafely(url: string, label: string) {

packages/theme/src/cli/utilities/theme-environment/remote-theme-watcher.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export async function reconcileAndPollThemeEditorChanges(
2222
ignore: string[]
2323
only: string[]
2424
},
25+
rejectBackgroundJob: (reason?: unknown) => void,
2526
): Promise<{
2627
updatedRemoteChecksumsPromise: Promise<Checksum[]>
2728
workPromise: Promise<void>
@@ -33,7 +34,14 @@ export async function reconcileAndPollThemeEditorChanges(
3334

3435
const updatedRemoteChecksumsPromise = workPromise.then(async () => {
3536
const updatedRemoteChecksums = await fetchChecksums(targetTheme.id, session)
36-
pollThemeEditorChanges(targetTheme, session, updatedRemoteChecksums, localThemeFileSystem, options)
37+
pollThemeEditorChanges(
38+
targetTheme,
39+
session,
40+
updatedRemoteChecksums,
41+
localThemeFileSystem,
42+
options,
43+
rejectBackgroundJob,
44+
)
3745
return updatedRemoteChecksums
3846
})
3947

packages/theme/src/cli/utilities/theme-environment/theme-environment.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,15 @@ describe('setupDevServer', () => {
158158
[],
159159
context.localThemeFileSystem,
160160
{noDelete: true, ...filters},
161+
expect.anything(),
161162
)
163+
// This is the best way I could think of verifying the rejectBackgroundJob
164+
// Verify the rejectBackgroundJob callback is a function accepting one argument
165+
const callArgs = vi.mocked(reconcileAndPollThemeEditorChanges).mock.calls[0]
166+
const rejectCallback = callArgs?.[5]
167+
expect(rejectCallback).toBeTypeOf('function')
168+
// Reject callbacks take 1 argument: the rejection reason
169+
expect(rejectCallback).toHaveLength(1)
162170
})
163171

164172
test('should skip deletion of remote files if noDelete flag is passed', async () => {

packages/theme/src/cli/utilities/theme-environment/theme-environment.ts

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,41 @@ import {getProxyHandler} from './proxy.js'
55
import {reconcileAndPollThemeEditorChanges} from './remote-theme-watcher.js'
66
import {uploadTheme} from '../theme-uploader.js'
77
import {renderTasksToStdErr} from '../theme-ui.js'
8-
import {createAbortCatchError} from '../errors.js'
8+
import {renderThrownError} from '../errors.js'
99
import {createApp, defineEventHandler, defineLazyEventHandler, toNodeListener, handleCors} from 'h3'
1010
import {fetchChecksums} from '@shopify/cli-kit/node/themes/api'
1111
import {createServer} from 'node:http'
1212
import type {Checksum, Theme} from '@shopify/cli-kit/node/themes/types'
1313
import type {DevServerContext} from './types.js'
1414

15+
// Polyfill for Promise.withResolvers
16+
// Can remove once our minimum supported Node version is 22
17+
interface PromiseWithResolvers<T> {
18+
promise: Promise<T>
19+
resolve: (value: T | PromiseLike<T>) => void
20+
reject: (reason?: unknown) => void
21+
}
22+
23+
function promiseWithResolvers<T>(): PromiseWithResolvers<T> {
24+
if (typeof Promise.withResolvers === 'function') {
25+
return Promise.withResolvers<T>()
26+
}
27+
28+
let resolve!: (value: T | PromiseLike<T>) => void
29+
let reject!: (reason?: unknown) => void
30+
const promise = new Promise<T>((_resolve, _reject) => {
31+
resolve = _resolve
32+
reject = _reject
33+
})
34+
35+
return {promise, resolve, reject}
36+
}
37+
1538
export function setupDevServer(theme: Theme, ctx: DevServerContext) {
39+
const {promise: backgroundJobPromise, reject: rejectBackgroundJob} = promiseWithResolvers<never>()
40+
1641
const watcherPromise = setupInMemoryTemplateWatcher(theme, ctx)
17-
const envSetup = ensureThemeEnvironmentSetup(theme, ctx)
42+
const envSetup = ensureThemeEnvironmentSetup(theme, ctx, rejectBackgroundJob)
1843
const workPromise = Promise.all([watcherPromise, envSetup.workPromise]).then(() =>
1944
ctx.localThemeFileSystem.startWatcher(theme.id.toString(), ctx.session),
2045
)
@@ -25,16 +50,25 @@ export function setupDevServer(theme: Theme, ctx: DevServerContext) {
2550
serverStart: server.start,
2651
dispatchEvent: server.dispatch,
2752
renderDevSetupProgress: envSetup.renderProgress,
53+
backgroundJobPromise,
2854
}
2955
}
3056

31-
function ensureThemeEnvironmentSetup(theme: Theme, ctx: DevServerContext) {
32-
const abort = createAbortCatchError('Failed to perform the initial theme synchronization.')
57+
function ensureThemeEnvironmentSetup(
58+
theme: Theme,
59+
ctx: DevServerContext,
60+
rejectBackgroundJob: (reason?: unknown) => void,
61+
) {
62+
const abort = (error: Error): never => {
63+
renderThrownError('Failed to perform the initial theme synchronization.', error)
64+
rejectBackgroundJob(error)
65+
throw error
66+
}
3367

3468
const remoteChecksumsPromise = fetchChecksums(theme.id, ctx.session).catch(abort)
3569

3670
const reconcilePromise = remoteChecksumsPromise
37-
.then((remoteChecksums) => handleThemeEditorSync(theme, ctx, remoteChecksums))
71+
.then((remoteChecksums) => handleThemeEditorSync(theme, ctx, remoteChecksums, rejectBackgroundJob))
3872
.catch(abort)
3973

4074
const uploadPromise = reconcilePromise
@@ -74,16 +108,24 @@ function handleThemeEditorSync(
74108
theme: Theme,
75109
ctx: DevServerContext,
76110
remoteChecksums: Checksum[],
111+
rejectBackgroundJob: (reason?: unknown) => void,
77112
): Promise<{
78113
updatedRemoteChecksumsPromise: Promise<Checksum[]>
79114
workPromise: Promise<void>
80115
}> {
81116
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-
})
117+
return reconcileAndPollThemeEditorChanges(
118+
theme,
119+
ctx.session,
120+
remoteChecksums,
121+
ctx.localThemeFileSystem,
122+
{
123+
noDelete: ctx.options.noDelete,
124+
ignore: ctx.options.ignore,
125+
only: ctx.options.only,
126+
},
127+
rejectBackgroundJob,
128+
)
87129
} else {
88130
return Promise.resolve({
89131
updatedRemoteChecksumsPromise: Promise.resolve(remoteChecksums),

packages/theme/src/cli/utilities/theme-environment/theme-polling.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export function pollThemeEditorChanges(
2121
remoteChecksum: Checksum[],
2222
localFileSystem: ThemeFileSystem,
2323
options: PollingOptions,
24+
rejectBackgroundJob: (reason?: unknown) => void,
2425
) {
2526
outputDebug('Listening for changes in the theme editor')
2627

@@ -51,14 +52,12 @@ export function pollThemeEditorChanges(
5152
}
5253

5354
if (failedPollingAttempts >= maxPollingAttempts) {
54-
renderFatalError(
55-
new AbortError(
56-
'Too many polling errors...',
57-
'Please check the errors above and ensure you have a stable internet connection.',
58-
),
55+
const fatalError = new AbortError(
56+
'Too many polling errors...',
57+
'Please check the errors above and ensure you have a stable internet connection.',
5958
)
60-
61-
process.exit(1)
59+
renderFatalError(fatalError)
60+
rejectBackgroundJob(fatalError)
6261
}
6362

6463
return latestChecksums

0 commit comments

Comments
 (0)