diff --git a/.changeset/fix-bundle-ui-step.md b/.changeset/fix-bundle-ui-step.md new file mode 100644 index 0000000000..19434812a9 --- /dev/null +++ b/.changeset/fix-bundle-ui-step.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Fix `shopify app build` for some UI extensions 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 c87aadb12a..1a7a7a0cdd 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 @@ -1,4 +1,5 @@ import {executeBundleUIStep} from './bundle-ui-step.js' +import * as generateManifest from './include-assets/generate-manifest.js' import * as buildExtension from '../extension.js' import {BundleUIStep, BuildContext} from '../client-steps.js' import {ExtensionInstance} from '../../../models/extensions/extension-instance.js' @@ -7,6 +8,7 @@ import * as fs from '@shopify/cli-kit/node/fs' vi.mock('@shopify/cli-kit/node/fs') vi.mock('../extension.js') +vi.mock('./include-assets/generate-manifest.js') describe('executeBundleUIStep', () => { let mockContext: BuildContext @@ -46,4 +48,34 @@ 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() + }) + + test('skips manifest generation when local and bundle output directories resolve to the same path', async () => { + const stepWithManifest: BundleUIStep = { + id: 'bundle-ui', + name: 'Bundle UI Extension', + type: 'bundle_ui', + config: {generatesAssetsManifest: true}, + } + mockContext.extension.outputPath = '/test/./extension/dist/handle.js' + mockContext.extension.configuration = { + extension_points: [ + {target: 'admin.product-details.action.render', build_manifest: {assets: {main: {filepath: 'main.js'}}}}, + ], + } as ExtensionInstance['configuration'] + vi.mocked(buildExtension.buildUIExtension).mockResolvedValue('/test/extension/dist/handle.js') + + await executeBundleUIStep(stepWithManifest, mockContext) + + expect(fs.copyFile).not.toHaveBeenCalled() + expect(generateManifest.createOrUpdateManifestFile).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 ace25649bb..580550b053 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 { @@ -26,9 +26,11 @@ 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) { - await copyFile(localOutputDir, bundleOutputDir) - } + + // 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 + + await copyFile(localOutputDir, bundleOutputDir) if (!step.config?.generatesAssetsManifest) return