diff --git a/.changeset/deploy-no-build-missing-web-pixel-bundle.md b/.changeset/deploy-no-build-missing-web-pixel-bundle.md new file mode 100644 index 00000000000..65d1741540c --- /dev/null +++ b/.changeset/deploy-no-build-missing-web-pixel-bundle.md @@ -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. diff --git a/packages/app/src/cli/services/build/errors.ts b/packages/app/src/cli/services/build/errors.ts new file mode 100644 index 00000000000..ab80157597d --- /dev/null +++ b/packages/app/src/cli/services/build/errors.ts @@ -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.', + ) + } +} diff --git a/packages/app/src/cli/services/build/extension.test.ts b/packages/app/src/cli/services/build/extension.test.ts index a1da883761d..66b86170494 100644 --- a/packages/app/src/cli/services/build/extension.test.ts +++ b/packages/app/src/cli/services/build/extension.test.ts @@ -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' @@ -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}`) + }) + }) +}) diff --git a/packages/app/src/cli/services/build/extension.ts b/packages/app/src/cli/services/build/extension.ts index 1fd1c06ea20..885118cafc5 100644 --- a/packages/app/src/cli/services/build/extension.ts +++ b/packages/app/src/cli/services/build/extension.ts @@ -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' @@ -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' @@ -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) diff --git a/packages/app/src/cli/services/build/steps/bundle-ui-step.ts b/packages/app/src/cli/services/build/steps/bundle-ui-step.ts index 738964da6e4..f9acb3b46ff 100644 --- a/packages/app/src/cli/services/build/steps/bundle-ui-step.ts +++ b/packages/app/src/cli/services/build/steps/bundle-ui-step.ts @@ -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 { @@ -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