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