Skip to content

Commit 07ccbcb

Browse files
Preserve fatal errors when all theme deletions fail
1 parent ddcc5b9 commit 07ccbcb

2 files changed

Lines changed: 34 additions & 1 deletion

File tree

‎packages/theme/src/cli/services/delete.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,37 @@ test('does not remove local state when remote deletion fails', async () => {
201201
expect(removeDevelopmentTheme).not.toHaveBeenCalled()
202202
})
203203

204+
test.each([false, true])('preserves the first fatal error when all deletions fail (multiple: %s)', async (multiple) => {
205+
const error = new AbortError('Missing theme access scope', 'Check the access token scopes.', [
206+
'Request the required theme access scope.',
207+
])
208+
error.code = 'MISSING_THEME_SCOPE'
209+
error.customSections = [{title: 'Required access', body: 'write_themes'}]
210+
error.details = {requiredScope: 'write_themes'}
211+
vi.mocked(findThemes).mockResolvedValue(multiple ? [theme1, theme2] : [theme1])
212+
vi.mocked(themeDelete).mockRejectedValueOnce(error).mockRejectedValueOnce(new Error('Another deletion failed'))
213+
214+
await expect(executeDelete(session, {...options, themes: ['1', '2'], force: true})).rejects.toBe(error)
215+
expect(error.details).toEqual({requiredScope: 'write_themes'})
216+
})
217+
218+
test('includes each theme failure when ordinary errors cause total failure', async () => {
219+
vi.mocked(findThemes).mockResolvedValue([theme1, theme2])
220+
vi.mocked(themeDelete)
221+
.mockRejectedValueOnce(new Error('First deletion failed'))
222+
.mockRejectedValueOnce(new Error('Second deletion failed'))
223+
224+
await expect(executeDelete(session, {...options, themes: ['1', '2'], force: true})).rejects.toMatchObject({
225+
message: 'First deletion failed',
226+
details: {
227+
errors: [
228+
{themeId: '1', message: 'First deletion failed'},
229+
{themeId: '2', message: 'Second deletion failed'},
230+
],
231+
},
232+
})
233+
})
234+
204235
test('preserves the environment label in terminal output', () => {
205236
renderThemeDeleteResult({status: 'success', themes: [{...theme1, shop: session.storeFqdn}]}, 'text', {
206237
store: session.storeFqdn,

‎packages/theme/src/cli/services/delete.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import {pluralize} from '@shopify/cli-kit/common/string'
1515
import {Theme} from '@shopify/cli-kit/node/themes/types'
1616
import {isDevelopmentTheme} from '@shopify/cli-kit/node/themes/utils'
17-
import {AbortError} from '@shopify/cli-kit/node/error'
17+
import {AbortError, FatalError} from '@shopify/cli-kit/node/error'
1818
import {outputWarn} from '@shopify/cli-kit/node/output'
1919

2020
interface DeleteOptions {
@@ -67,6 +67,8 @@ export async function themesDelete(
6767
return [{themeId: themes[index]!.id, message: error instanceof Error ? error.message : String(error)}]
6868
})
6969
if (errors.length > 0 && deletedThemes.length === 0) {
70+
const firstError: unknown = deletions.find((deletion) => deletion.status === 'rejected')?.reason
71+
if (firstError instanceof FatalError) throw firstError
7072
const error = new AbortError(errors[0]!.message)
7173
error.details = {errors: errors.map(({themeId, message}) => ({themeId: String(themeId), message}))}
7274
throw error

0 commit comments

Comments
 (0)