Skip to content

fix(workshop-utils): Windows path compatibility in diff pipeline#608

Merged
kentcdodds merged 1 commit into
epicweb-dev:mainfrom
AlemTuzlak:fix/windows-path-compat-in-diff
May 17, 2026
Merged

fix(workshop-utils): Windows path compatibility in diff pipeline#608
kentcdodds merged 1 commit into
epicweb-dev:mainfrom
AlemTuzlak:fix/windows-path-compat-in-diff

Conversation

@AlemTuzlak
Copy link
Copy Markdown
Contributor

What

Three small Windows path fixes in packages/workshop-utils/src/diff.server.ts.

Why

On Windows, path.join and path.relative return backslash-separated paths. The diff pipeline assumed POSIX-style forward slashes in three places, so the Diff tab in any workshop crashes for Windows users with:

TypeError: Cannot read properties of undefined (reading 'trim')
    at processPatch (.../@pierre/diffs/.../parsePatchFiles.ts:90)
    at parsePatchFiles (.../parsePatchFiles.ts:297)
    at children (.../@epic-web/workshop-app/app/components/diff.tsx:368)

The root cause chain:

  1. `git diff --no-index` quotes any path that contains a backslash. That quoted output trips `@pierre/diffs`'s patch parser, which only reads the unquoted capture groups of its filename regex and `.trim()`s an `undefined`.
  2. `copyUnignoredFiles` passes the result of `path.relative(...)` directly to the `ignore` package. The `ignore` package only understands POSIX paths, so any gitignore pattern with a slash in it (`/build/`, `/.react-router`, etc.) silently fails to match.
  3. `.slice(1)` was used to strip a leading slash from absolute paths. On POSIX (`/tmp/foo` → `tmp/foo`) this works; on Windows (`C:/Users/foo` → `:/Users/foo`) it strips the drive letter, breaking the subsequent `replaceAll` substitutions.

Changes

  1. `copyUnignoredFiles` — normalise `path.relative(...)` to POSIX before passing to `ignore`. Without this, gitignore patterns containing slashes silently fail to match on Windows.

  2. `prepareForDiff` — normalise `app1CopyPath` / `app2CopyPath` to POSIX after `path.join`. Git now sees forward-slash paths and emits unquoted output, which the patch parser can handle.

  3. `getDiffPatchImpl` and `getDiffOutputWithRelativePaths` — replace `.slice(1)` with `.replace(/^//, '')` so Windows paths (no leading slash) keep their drive letter. The `a${absPath}` patterns become `a/${relativePath}` so they line up with the slash that git always inserts, on both platforms.

A small `toPosixPath(p)` helper is defined locally in the file; happy to extract it to a shared module if you'd prefer.

Testing

  • POSIX behaviour unchanged. The new `a/${relative}` pattern produces the same search string as the old `a${absPath}` form (since absPath always starts with `/` on POSIX, you'd get `a/` in both cases). The new `replace(/^//, '')` produces the same result as the old `.slice(1)` whenever the input has exactly one leading slash.
  • Windows: verified locally against a workshop with exercise paths under `F:\projects...`. Before this PR the Diff tab crashes immediately; after, it renders.
  • Workshop-utils typecheck passes.
  • Existing test failures (`data-storage.test.ts` + a few others) are pre-existing Windows POSIX-path assumptions in unrelated modules, not introduced by this PR. Happy to file follow-ups for those.

Notes

The downstream `@pierre/diffs` parser also has a latent bug — its `diff --git` line destructure only reads the unquoted capture groups, even though its own regex has a quoted-path branch. Fixing this PR is sufficient to make the diff tab work because git no longer emits quoted paths, but the parser fix is worth pursuing upstream as well.

On Windows, `path.join` and `path.relative` return backslash-separated
paths. Three places in `diff.server.ts` assumed POSIX-style forward
slashes, which caused the Diff tab in any workshop to crash for Windows
users with:

    TypeError: Cannot read properties of undefined (reading 'trim')
        at processPatch (.../@pierre/diffs/.../parsePatchFiles.ts:90)

Root cause chain:

1. `copyUnignoredFiles` passes `path.relative(...)` directly to the
   `ignore` package, which only understands POSIX paths. Patterns with
   slashes like `**/build/` silently fail to match on Windows.

2. `prepareForDiff` builds temp copy paths with `path.join`. When
   passed to `git diff --no-index`, git quotes any path containing a
   backslash. The quoted output trips the downstream `@pierre/diffs`
   patch parser.

3. `getDiffPatchImpl` and `getDiffOutputWithRelativePaths` use
   `.slice(1)` to strip a leading slash. On POSIX (`/tmp/foo`) this
   works. On Windows (`C:/Users/foo`) it strips the drive letter.

This patch normalises paths to POSIX form wherever they cross into git,
the `ignore` package, or the patch-text replacements. The `a${path}`
replaceAll patterns are also changed to `a/${relative}` so they line up
with what git emits on both platforms.

No behaviour change on POSIX. On Windows the Diff tab now renders
instead of crashing.
@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 17, 2026

View your CI Pipeline Execution ↗ for commit f47550d

Command Status Duration Result
nx run-many --target typecheck ✅ Succeeded 19s View ↗
nx run-many --target build ✅ Succeeded 38s View ↗
nx lint ✅ Succeeded 31s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-17 17:03:07 UTC

@kentcdodds
Copy link
Copy Markdown
Member

Bugbot review

@kentcdodds kentcdodds merged commit d95cc21 into epicweb-dev:main May 17, 2026
9 checks passed
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.

2 participants