From a64133faddf56326a07443b53230fb73641c27a9 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Fri, 22 May 2026 10:21:01 +0200 Subject: [PATCH 1/2] chore(cli-test): exclude node_modules on Windows and tolerate dangling symlinks The copy-fixtures filter used `src.split('/').pop()` which fails on Windows (paths use `\`), so `node_modules` was copied into the bundled fixtures dir during the Windows build. Node's `fs.cp` resolves symlinks to absolute paths, and since `fixtures/**` is a turbo build output the Windows-absolute symlinks were cached and replayed on Linux, where they became dangling and crashed `testFixture` setup with ENOENT. Switch the filter to `path.basename` so it's platform-aware, and skip dangling symlinks in `testFixture` defensively so cross-OS cache replay can't ever crash fixture loading again. --- .../@sanity/cli-test/scripts/copy-fixtures.ts | 4 ++-- .../@sanity/cli-test/src/test/testFixture.ts | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/@sanity/cli-test/scripts/copy-fixtures.ts b/packages/@sanity/cli-test/scripts/copy-fixtures.ts index e3c9b527c..3447e9489 100644 --- a/packages/@sanity/cli-test/scripts/copy-fixtures.ts +++ b/packages/@sanity/cli-test/scripts/copy-fixtures.ts @@ -6,7 +6,7 @@ // eslint-disable-next-line n/no-unsupported-features/node-builtins import {cp, mkdir, readFile, writeFile} from 'node:fs/promises' -import {dirname, join, resolve} from 'node:path' +import {basename, dirname, join, resolve} from 'node:path' import {parse as parseYaml} from 'yaml' @@ -89,7 +89,7 @@ async function copyFixtures() { // Copy the fixture, excluding node_modules, .turbo and (unless specified) dist directories await cp(sourceDir, targetDir, { filter: (src) => { - const name = src.split('/').pop() + const name = basename(src) return ( name !== 'node_modules' && name !== '.turbo' && diff --git a/packages/@sanity/cli-test/src/test/testFixture.ts b/packages/@sanity/cli-test/src/test/testFixture.ts index dd4a3a05a..7fc95e423 100644 --- a/packages/@sanity/cli-test/src/test/testFixture.ts +++ b/packages/@sanity/cli-test/src/test/testFixture.ts @@ -155,8 +155,20 @@ export async function testFixture( const srcPath = join(srcNodeModules, entry.name) // Follow symlinks (e.g. pnpm's `node_modules/` → `.pnpm/...`) so // they get a 'dir' type on Windows — a file-typed symlink to a directory - // throws EPERM on stat. - const targetStats = entry.isSymbolicLink() ? await stat(srcPath) : entry + // throws EPERM on stat. Skip dangling symlinks: stat throws ENOENT when + // the target is missing (can happen with cross-OS turbo cache replay + // where Windows-absolute symlink targets don't resolve on Linux). + let targetStats: {isDirectory: () => boolean} | undefined + if (entry.isSymbolicLink()) { + try { + targetStats = await stat(srcPath) + } catch (err) { + if (err instanceof Error && 'code' in err && err.code === 'ENOENT') continue + throw err + } + } else { + targetStats = entry + } await symlink( srcPath, join(destNodeModules, entry.name), From 489a86aad4f3b33cb73da8ee4a9785cb00122a2c Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Fri, 22 May 2026 10:30:12 +0200 Subject: [PATCH 2/2] chore(cli-test): tighten targetStats type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop `| undefined` from `targetStats` — every path reaching `symlink(...)` definitively assigns it (the symlink branch assigns, continues, or throws; the else branch assigns), so the wider type was misleading. --- packages/@sanity/cli-test/src/test/testFixture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@sanity/cli-test/src/test/testFixture.ts b/packages/@sanity/cli-test/src/test/testFixture.ts index 7fc95e423..eb089b365 100644 --- a/packages/@sanity/cli-test/src/test/testFixture.ts +++ b/packages/@sanity/cli-test/src/test/testFixture.ts @@ -158,7 +158,7 @@ export async function testFixture( // throws EPERM on stat. Skip dangling symlinks: stat throws ENOENT when // the target is missing (can happen with cross-OS turbo cache replay // where Windows-absolute symlink targets don't resolve on Linux). - let targetStats: {isDirectory: () => boolean} | undefined + let targetStats: {isDirectory: () => boolean} if (entry.isSymbolicLink()) { try { targetStats = await stat(srcPath)