From eb0bb8165e426005952b1e0d0cab2693d1dbc330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Tue, 21 Apr 2026 13:13:44 +0200 Subject: [PATCH 1/2] Fix `shopify app build` failing for UI extensions with same-path copy The bundle_ui client step always called copyFile to move locally bundled UI extension output into the bundle directory. During a plain `shopify app build` the extension's outputPath was never reassigned to a separate bundle directory, so source and destination collapsed onto the same directory and fs-extra threw 'Source and destination must not be the same.' Skip the copy when both paths share a directory. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/fix-bundle-ui-same-path-copy.md | 5 ++ .../build/steps/bundle-ui-step.test.ts | 60 +++++++++++++++++++ .../services/build/steps/bundle-ui-step.ts | 8 ++- 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-bundle-ui-same-path-copy.md create mode 100644 packages/app/src/cli/services/build/steps/bundle-ui-step.test.ts diff --git a/.changeset/fix-bundle-ui-same-path-copy.md b/.changeset/fix-bundle-ui-same-path-copy.md new file mode 100644 index 0000000000..4a82a970c1 --- /dev/null +++ b/.changeset/fix-bundle-ui-same-path-copy.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Fix `shopify app build` failing for UI extensions with `Source and destination must not be the same`. The bundle_ui step was unconditionally copying the bundled output into the extension's bundle directory, but during a plain build those paths are the same directory. 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 new file mode 100644 index 0000000000..18aa974214 --- /dev/null +++ b/packages/app/src/cli/services/build/steps/bundle-ui-step.test.ts @@ -0,0 +1,60 @@ +import {executeBundleUIStep} from './bundle-ui-step.js' +import * as buildExtension from '../extension.js' +import {BundleUIStep, BuildContext} from '../client-steps.js' +import {ExtensionInstance} from '../../../models/extensions/extension-instance.js' +import {describe, expect, test, vi, beforeEach} from 'vitest' +import * as fs from '@shopify/cli-kit/node/fs' + +vi.mock('@shopify/cli-kit/node/fs') +vi.mock('../extension.js') + +describe('executeBundleUIStep', () => { + let mockContext: BuildContext + + beforeEach(() => { + mockContext = { + extension: { + directory: '/test/extension', + outputPath: '/test/extension/dist/handle.js', + configuration: {}, + } as ExtensionInstance, + options: { + stdout: {write: vi.fn()} as any, + stderr: {write: vi.fn()} as any, + app: {} as any, + environment: 'production', + }, + stepResults: new Map(), + } + }) + + const step: BundleUIStep = { + id: 'bundle-ui', + name: 'Bundle UI Extension', + type: 'bundle_ui', + config: {generatesAssetsManifest: false}, + } + + test('skips the copy when local and bundle output directories are identical', async () => { + // Given + vi.mocked(buildExtension.buildUIExtension).mockResolvedValue('/test/extension/dist/handle.js') + + // When + await executeBundleUIStep(step, mockContext) + + // Then — fs-extra would throw "Source and destination must not be the same" + expect(fs.copyFile).not.toHaveBeenCalled() + }) + + test('copies when local and bundle output directories differ', async () => { + // Given + mockContext.extension.outputPath = '/bundle/handle/handle.js' + vi.mocked(buildExtension.buildUIExtension).mockResolvedValue('/test/extension/dist/handle.js') + + // When + await executeBundleUIStep(step, mockContext) + + // Then + expect(fs.copyFile).toHaveBeenCalledWith('/test/extension/dist', '/bundle/handle') + }) +}) 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 81174b8a4d..0c71029ced 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 @@ -21,8 +21,12 @@ interface ExtensionPointWithBuildManifest { export async function executeBundleUIStep(step: BundleUIStep, context: BuildContext): Promise { const config = context.extension.configuration const localOutputPath = await buildUIExtension(context.extension, context.options) - // Copy the locally built files into the bundle - await copyFile(dirname(localOutputPath), dirname(context.extension.outputPath)) + // When invoked outside a bundle directory (e.g. `shopify app build`), localOutputPath and outputPath collapse onto the same directory; fs-extra rejects same-path copies. + const localOutputDir = dirname(localOutputPath) + const bundleOutputDir = dirname(context.extension.outputPath) + if (localOutputDir !== bundleOutputDir) { + await copyFile(localOutputDir, bundleOutputDir) + } if (!step.config?.generatesAssetsManifest) return From 1d6b7566b73c46170616dffd783379f549d3c045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Tue, 21 Apr 2026 13:15:25 +0200 Subject: [PATCH 2/2] Remove changeset Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/fix-bundle-ui-same-path-copy.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/fix-bundle-ui-same-path-copy.md diff --git a/.changeset/fix-bundle-ui-same-path-copy.md b/.changeset/fix-bundle-ui-same-path-copy.md deleted file mode 100644 index 4a82a970c1..0000000000 --- a/.changeset/fix-bundle-ui-same-path-copy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@shopify/app': patch ---- - -Fix `shopify app build` failing for UI extensions with `Source and destination must not be the same`. The bundle_ui step was unconditionally copying the bundled output into the extension's bundle directory, but during a plain build those paths are the same directory.