diff --git a/.changeset/fix-bundle-ui-step-path-normalization.md b/.changeset/fix-bundle-ui-step-path-normalization.md new file mode 100644 index 00000000000..532dab38a51 --- /dev/null +++ b/.changeset/fix-bundle-ui-step-path-normalization.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Fix `shopify app build` intermittently failing with "Source and destination must not be the same" on UI extensions when the local esbuild output directory and the bundle output directory resolve to the same path but differ as strings (e.g. due to `.` segments, trailing slashes, or path joining quirks). The same-path guard now normalizes both paths via `resolvePath` before comparison. diff --git a/packages/app/src/cli/services/build/steps/bundle-ui-step.test.ts b/packages/app/src/cli/services/build/steps/bundle-ui-step.test.ts index 18aa9742142..1f3d905db63 100644 --- a/packages/app/src/cli/services/build/steps/bundle-ui-step.test.ts +++ b/packages/app/src/cli/services/build/steps/bundle-ui-step.test.ts @@ -57,4 +57,13 @@ describe('executeBundleUIStep', () => { // Then expect(fs.copyFile).toHaveBeenCalledWith('/test/extension/dist', '/bundle/handle') }) + + test('skips the copy when local and bundle output directories resolve to the same path but differ as strings', async () => { + mockContext.extension.outputPath = '/test/./extension/dist/handle.js' + vi.mocked(buildExtension.buildUIExtension).mockResolvedValue('/test/extension/dist/handle.js') + + await executeBundleUIStep(step, mockContext) + + expect(fs.copyFile).not.toHaveBeenCalled() + }) }) 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 5eae48ab71d..b77eb1fa1e3 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 @@ -2,7 +2,7 @@ import {createOrUpdateManifestFile} from './include-assets/generate-manifest.js' import {buildUIExtension} from '../extension.js' import {BuildManifest} from '../../../models/extensions/specifications/ui_extension.js' import {copyFile} from '@shopify/cli-kit/node/fs' -import {dirname, joinPath} from '@shopify/cli-kit/node/path' +import {dirname, joinPath, resolvePath} from '@shopify/cli-kit/node/path' import type {BundleUIStep, BuildContext} from '../client-steps.js' interface ExtensionPointWithBuildManifest { @@ -27,7 +27,7 @@ export async function executeBundleUIStep(step: BundleUIStep, context: BuildCont const bundleOutputDir = step.config?.bundleFolder ? joinPath(dirname(context.extension.outputPath), step.config.bundleFolder) : dirname(context.extension.outputPath) - if (localOutputDir !== bundleOutputDir) { + if (resolvePath(localOutputDir) !== resolvePath(bundleOutputDir)) { await copyFile(localOutputDir, bundleOutputDir) }