Skip to content

Normalize paths in bundle-ui-step same-path guard#7388

Merged
vctrchu merged 2 commits intomainfrom
vchu/fix-bundle-ui-step-path-normalization
Apr 24, 2026
Merged

Normalize paths in bundle-ui-step same-path guard#7388
vctrchu merged 2 commits intomainfrom
vchu/fix-bundle-ui-step-path-normalization

Conversation

@vctrchu
Copy link
Copy Markdown
Contributor

@vctrchu vctrchu commented Apr 23, 2026

Resolves https://github.com/shop/issues-retail/issues/28335

Companion ui-extension PR

Summary

shopify app build intermittently fails on UI extensions with:

image.png

The same-path guard in bundle-ui-step.ts compared localOutputDir and bundleOutputDir as raw strings. These are computed along different code paths:

  • localOutputDir = dirname(buildUIExtension(...)) — where esbuild writes
  • bundleOutputDir = dirname(extension.outputPath) or joinPath(dirname(extension.outputPath), bundleFolder) — copy destination

When both resolve to the same directory on disk but differ as strings (a . segment, a trailing slash, a path that went through a different joinPath path and came out non-canonical), the !== check slips through and fs-extra rejects the copy.

Fix

Normalize both sides with resolvePath before comparing:

-if (localOutputDir !== bundleOutputDir) {
+if (resolvePath(localOutputDir) !== resolvePath(bundleOutputDir)) {
   await copyFile(localOutputDir, bundleOutputDir)
 }

Two-line change. Same code path that the existing test at bundle-ui-step.test.ts:38-47 was already asserting skips the copy — the guard just wasn't robust to non-canonical path shapes.

Scope

  • Pure guard normalization. copyFile is only invoked in the "different directories" case (unchanged).
  • No behavior change when the two directories are genuinely different — resolvePath is idempotent on canonical paths.
  • No runtime-resolution of symlinks (keeps current semantics — we rely on string equality of resolved paths, not realpathSync).

How to reproduce on main

Any local setup where extension.outputPath and buildUIExtension's return value compute to the same directory via different string shapes will hit this. It surfaced for me while running pnpm shopify app build --path <sibling-app> against a test extension that links @shopify/ui-extensions via a pnpm.overrides link: path — the esbuild output path came back in a different normalization from the extension output path, and the existing guard missed it.

Test plan

  • Existing bundle-ui-step.test.ts still passes (unit test mocks fs.copyFile; the behavior asserted — "skips the copy when local and bundle output directories are identical" — is preserved and now more robust).
  • Full shopify app build on an affected extension completes end-to-end (previously failed at the bundle step).
  • Reviewer: any path shape that was previously distinct-by-string-comparison but resolvePath-equal would now skip the copy. Confirm no intended code path relied on that (I could not find one — the branch is explicitly guarding against fs-extra's same-path rejection, and every caller I traced expects a no-op when the dirs collapse).

Rollback

Revert the two-line change. No migration required.

The same-path guard at bundle-ui-step.ts compared `localOutputDir` and
`bundleOutputDir` as raw strings. In practice these two values are
computed along different code paths (`dirname(buildUIExtension())` vs
`dirname(extension.outputPath)` or `joinPath(..., bundleFolder)`) and
can resolve to the same filesystem directory while differing as strings
— e.g. when one path has a `.` segment, a trailing slash, or otherwise
non-canonical shape. When that happens, the guard slips through and
fs-extra rejects the copy with "Source and destination must not be the
same".

Normalize both sides via `resolvePath` before comparing so the guard
catches any string variant that maps to the same directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vctrchu vctrchu marked this pull request as ready for review April 23, 2026 22:51
@vctrchu vctrchu requested review from a team as code owners April 23, 2026 22:51
Copilot AI review requested due to automatic review settings April 23, 2026 22:51
@vctrchu vctrchu self-assigned this Apr 23, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an intermittent shopify app build failure for UI extensions where the “same-path” copy guard in the bundle UI build step could miss equivalent paths that differ only by non-canonical string shape (e.g., . segments), causing fs-extra to throw “Source and destination must not be the same.”

Changes:

  • Normalize localOutputDir and bundleOutputDir via resolvePath before comparing, preventing same-path copy attempts.
  • Add a changeset documenting the patch release and the failure mode addressed.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
packages/app/src/cli/services/build/steps/bundle-ui-step.ts Normalizes both directories before the same-path guard comparison to avoid same-path copy errors.
.changeset/fix-bundle-ui-step-path-normalization.md Declares a patch release for @shopify/app and documents the fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/app/src/cli/services/build/steps/bundle-ui-step.ts
@vctrchu vctrchu force-pushed the vchu/fix-bundle-ui-step-path-normalization branch from 69fa7e0 to d5b9fab Compare April 23, 2026 22:59
Extends the same-path guard coverage with a case where localOutputDir
and bundleOutputDir are string-distinct but resolve to the same
directory (`/test/extension/dist` vs `/test/./extension/dist`). This
is the actual shape that triggered the original fs-extra "Source and
destination must not be the same" failure in the field — the existing
identical-strings test did not catch it because both the old and new
guard handle the trivial case.

Verified that this test fails against the pre-fix guard (`copyFile`
is called with distinct strings) and passes once the guard normalizes
both sides with `resolvePath`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vctrchu vctrchu force-pushed the vchu/fix-bundle-ui-step-path-normalization branch from d5b9fab to 35deef9 Compare April 23, 2026 23:00
@vctrchu vctrchu requested a review from isaacroldan April 24, 2026 00:03
@vctrchu vctrchu added this pull request to the merge queue Apr 24, 2026
Merged via the queue into main with commit 397d638 Apr 24, 2026
47 of 48 checks passed
@vctrchu vctrchu deleted the vchu/fix-bundle-ui-step-path-normalization branch April 24, 2026 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants