Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/deploy-no-build-missing-web-pixel-bundle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/app': patch
---

Show a clear error instead of ENOENT when `app deploy --no-build` finds no built web pixel bundle.
14 changes: 14 additions & 0 deletions packages/app/src/cli/services/build/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {AbortError} from '@shopify/cli-kit/node/error'

/**
* The build output doesn't exist, usually after `deploy --no-build`.
* A developer condition, not a CLI defect, so it stays out of crash reporting.
*/
export class MissingBuildOutputError extends AbortError {
constructor(outputPath: string) {
super(
`Couldn't find the build output at ${outputPath}`,
'If you passed --no-build, build the app first. Otherwise check that building the extension produces files at that path.',
)
}
}
28 changes: 26 additions & 2 deletions packages/app/src/cli/services/build/extension.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {buildFunctionExtension} from './extension.js'
import {testFunctionExtension} from '../../models/app/app.test-data.js'
import {buildFunctionExtension, buildUIExtension} from './extension.js'
import {testFunctionExtension, testUIExtension} from '../../models/app/app.test-data.js'
import {buildGraphqlTypes, buildJSFunction, runWasmOpt, runTrampoline} from '../function/build.js'
import {validateSchemaApiVersion} from '../function/schema-version.js'
import {FunctionConfigType} from '../../models/extensions/specifications/function.js'
Expand Down Expand Up @@ -528,3 +528,27 @@ describe('buildFunctionExtension', () => {
})
})
})

describe('buildUIExtension', () => {
test('aborts with a clear error when skipBuild is set and the web pixel bundle is missing', async () => {
await fs.inTemporaryDirectory(async (tmpDir) => {
// Given
const extension = await testUIExtension({type: 'web_pixel_extension', directory: tmpDir})
const options = {
stdout: {write: vi.fn()} as unknown as Writable,
stderr: {write: vi.fn()} as unknown as Writable,
app: {} as any,
environment: 'production' as const,
skipBuild: true,
}
const localOutputPath = joinPath(tmpDir, extension.outputRelativePath)

// When
const result = buildUIExtension(extension, options)

// Then
await expect(result).rejects.toThrowError(AbortError)
await expect(result).rejects.toThrow(`Couldn't find the build output at ${localOutputPath}`)
})
})
})
8 changes: 7 additions & 1 deletion packages/app/src/cli/services/build/extension.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {formatBundleSize} from './bundle-size.js'
import {MissingBuildOutputError} from './errors.js'
import {AppInterface} from '../../models/app/app.js'
import {bundleExtension} from '../extensions/bundle.js'
import {buildGraphqlTypes, buildJSFunction, runTrampoline, runWasmOpt} from '../function/build.js'
Expand All @@ -12,7 +13,7 @@ import {AbortError, AbortSilentError} from '@shopify/cli-kit/node/error'
import lockfile from 'proper-lockfile'
import {dirname, joinPath} from '@shopify/cli-kit/node/path'
import {outputDebug} from '@shopify/cli-kit/node/output'
import {readFile, touchFile, writeFile, fileExistsSync} from '@shopify/cli-kit/node/fs'
import {fileExists, readFile, touchFile, writeFile, fileExistsSync} from '@shopify/cli-kit/node/fs'
import {Writable} from 'stream'
// eslint-disable-next-line no-restricted-imports
import {resolve} from 'node:path'
Expand Down Expand Up @@ -135,6 +136,11 @@ export async function buildUIExtension(extension: ExtensionInstance, options: Ex
}
}

// Validations such as the web pixel size check read the output file.
if (options.skipBuild && !(await fileExists(localOutputPath))) {
throw new MissingBuildOutputError(localOutputPath)
}

await extension.buildValidation({outputPath: localOutputPath})

const duration = Math.round(performance.now() - startTime)
Expand Down
10 changes: 3 additions & 7 deletions packages/app/src/cli/services/build/steps/bundle-ui-step.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {createOrUpdateManifestFile} from './include-assets/generate-manifest.js'
import {buildUIExtension} from '../extension.js'
import {MissingBuildOutputError} from '../errors.js'
import {BuildManifest} from '../../../models/extensions/specifications/ui_extension.js'
import {copyFile, fileExists, glob, mkdir} from '@shopify/cli-kit/node/fs'
import {dirname, joinPath, relativePath, resolvePath} from '@shopify/cli-kit/node/path'
import {AbortError} from '@shopify/cli-kit/node/error'
import type {BundleUIStep, BuildContext} from '../client-steps.js'

interface ExtensionPointWithBuildManifest {
Expand Down Expand Up @@ -31,13 +31,9 @@ export async function executeBundleUIStep(step: BundleUIStep, context: BuildCont
// If the final output path is the same as the local one: don't copy the results and don't generate manifests.
if (resolvePath(localOutputDir) === resolvePath(bundleOutputDir)) return

// Nothing was built, usually `deploy --no-build`. A developer condition, not a
// CLI defect, so keep it out of crash reporting.
// Nothing was built, usually `deploy --no-build`.
if (!(await fileExists(localOutputDir))) {
throw new AbortError(
`Couldn't find the build output at ${localOutputDir}`,
'If you passed --no-build, build the app first. Otherwise check that building the extension produces files at that path.',
)
throw new MissingBuildOutputError(localOutputDir)
}

// Source maps stay in the extension's local output directory and are excluded
Expand Down
Loading