From 8348e19fd9e7d832f05acb44e42da3c424039165 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 13 May 2026 03:27:58 +0000 Subject: [PATCH] fix(ci): switch Windows install to hoisted linker; narrow FormData iter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pushing further to actually get Windows render verification green, not just work around it. ## What's wrong on Windows Bun 1.3's default `isolated` linker creates nested workspace junctions under `packages/*/node_modules/` on Windows GHA runners. Those junctions don't materialize reliably — Node's `realpathSync` returns `EPERM` on stat, and ESM resolution returns `ERR_MODULE_NOT_FOUND`. Every Windows build since PR #748 has tripped this in one of three places: - `packages/producer/build.mjs` importing `esbuild` - `packages/producer/scripts/generate-font-data.ts` reading `@fontsource/*` - `packages/producer` running `tsc` to emit `.d.ts`s Long-running bun bugs: oven-sh/bun#23615, #18354, #10146. ## Fix **1. `--linker=hoisted` for the Windows install step** (workflow change, Windows only). Hoisted layout puts deps as real directories at the workspace root + workspace package node_modules. No junctions, no Windows-specific path quirks. Linux CI keeps the default isolated linker; the lockfile is linker-agnostic so `--frozen-lockfile` is still valid. **2. Source-level FormData narrowing in `packages/core/src/studio-api/routes/files.ts`** (needed because the hoisted layout exposes a `@types/node@25` typecheck issue that the isolated layout hides). With v25 + an `onmessage` global in scope, the ambient `FormData.entries()` infers `[string, string]` instead of `[string, File | string]`, so the `value instanceof File` check breaks at `TS2358`. Cast the iterator to a `[string, FileLike | string]` shape and narrow via `typeof value === "string"`. Identical runtime behavior; works under both v24 (isolated layout, what Linux CI sees) and v25 (hoisted, what Windows CI sees with this change). ## Verification - `bun install --frozen-lockfile` (isolated, default): full build green - `bun install --frozen-lockfile --linker=hoisted`: full build green, core typecheck passes, `@hyperframes/core` 853 tests pass - Format/lint clean on both layouts --- .github/workflows/windows-render.yml | 22 ++++++++++++++++++-- packages/core/src/studio-api/routes/files.ts | 17 +++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/.github/workflows/windows-render.yml b/.github/workflows/windows-render.yml index 0a829147c..1490a97f2 100644 --- a/.github/workflows/windows-render.yml +++ b/.github/workflows/windows-render.yml @@ -150,7 +150,16 @@ jobs: - name: Install dependencies shell: pwsh - run: bun install --frozen-lockfile + # Force the pre-1.3 "hoisted" linker on Windows. Bun 1.3.x's default + # "isolated" linker creates nested workspace junctions that don't + # materialize reliably on Windows GHA runners — manifests as + # `Cannot find package 'esbuild'` walking up from + # `packages/producer/build.mjs` and `EPERM stat` on + # `packages/producer/node_modules/@fontsource/*` from generators. + # See oven-sh/bun#23615, #18354, #10146. Linux CI keeps isolated; + # the lockfile is linker-agnostic so this flag is safe with + # --frozen-lockfile. + run: bun install --frozen-lockfile --linker=hoisted - name: Build all packages shell: pwsh @@ -371,7 +380,16 @@ jobs: - name: Install dependencies shell: pwsh - run: bun install --frozen-lockfile + # Force the pre-1.3 "hoisted" linker on Windows. Bun 1.3.x's default + # "isolated" linker creates nested workspace junctions that don't + # materialize reliably on Windows GHA runners — manifests as + # `Cannot find package 'esbuild'` walking up from + # `packages/producer/build.mjs` and `EPERM stat` on + # `packages/producer/node_modules/@fontsource/*` from generators. + # See oven-sh/bun#23615, #18354, #10146. Linux CI keeps isolated; + # the lockfile is linker-agnostic so this flag is safe with + # --frozen-lockfile. + run: bun install --frozen-lockfile --linker=hoisted - name: Build shell: pwsh diff --git a/packages/core/src/studio-api/routes/files.ts b/packages/core/src/studio-api/routes/files.ts index 33474d253..e801d5b59 100644 --- a/packages/core/src/studio-api/routes/files.ts +++ b/packages/core/src/studio-api/routes/files.ts @@ -306,8 +306,21 @@ export function registerFileRoutes(api: Hono, adapter: StudioApiAdapter): void { const skipped: string[] = []; const invalid: Array<{ name: string; reason: string }> = []; - for (const [, value] of formData.entries()) { - if (!(value instanceof File)) continue; + // @types/node v25 narrows the ambient `FormData.entries()` to + // `[string, string]` in workspaces where another dep declares an + // `onmessage` global (it trips the worker branch of v25's conditional + // File type). At runtime the value is still `File | string` — cast the + // iterator so the rest of this block keeps type-checking on every + // bun-install layout (hoisted on Windows surfaces this; isolated on + // Linux happens to keep v24 in scope). + type FileLike = { + readonly name: string; + readonly size: number; + arrayBuffer(): Promise; + }; + const entries = formData.entries() as unknown as Iterable<[string, FileLike | string]>; + for (const [, value] of entries) { + if (typeof value === "string") continue; // Strip path separators — browsers may include directory components const name = value.name.split("/").pop()?.split("\\").pop() ?? "";