From 8e2548429271ed4dfbd813e9a205ec8108ef836d Mon Sep 17 00:00:00 2001 From: Kallinikos Milonakis Date: Fri, 11 Sep 2026 12:56:33 +0300 Subject: [PATCH 1/2] fix(assets-loader): join public paths with path.posix so remote assets work on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `convertToRemoteAssets` built the remote asset URL with `path.join(remotePublicPath, assetPath)`. On Windows that rewrites the separators to backslashes and, because `https:` is not a drive letter, prefixes the result with `.\` — so `new URL` throws `Invalid URL` and any bundle that contains a remote asset fails to build. `extractAssets` did the same to `publicPath`, which is served over HTTP and always uses forward slashes. Both values are URLs, not filesystem paths, so they are now joined with `path.posix.join`. On Linux and macOS `path.join` and `path.posix.join` are the same function, so the output there is unchanged. Before, on Windows: tester-app 4 failed / 10 passed, every failure `TypeError: Invalid URL` from the assets loader. After: 14 / 14. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018U3wcP5Qm39cas5MtVVabw --- .changeset/assets-loader-posix-public-path.md | 5 +++++ .../src/loaders/assetsLoader/convertToRemoteAssets.ts | 6 ++++-- packages/repack/src/loaders/assetsLoader/extractAssets.ts | 5 ++++- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 .changeset/assets-loader-posix-public-path.md diff --git a/.changeset/assets-loader-posix-public-path.md b/.changeset/assets-loader-posix-public-path.md new file mode 100644 index 000000000..32c73ba04 --- /dev/null +++ b/.changeset/assets-loader-posix-public-path.md @@ -0,0 +1,5 @@ +--- +"@callstack/repack": patch +--- + +Fix the assets loader failing on Windows. Public paths for both remote and extracted assets were joined with `path.join`, which rewrites the separators to backslashes on Windows; for remote assets that turned `https://…` into a string `new URL` rejects, so any bundle containing a remote asset failed with `TypeError: Invalid URL`. Both are URLs rather than filesystem paths and are now joined with `path.posix.join`, which produces the same output on Linux and macOS as before. diff --git a/packages/repack/src/loaders/assetsLoader/convertToRemoteAssets.ts b/packages/repack/src/loaders/assetsLoader/convertToRemoteAssets.ts index 3b3b08572..b8e95f1a4 100644 --- a/packages/repack/src/loaders/assetsLoader/convertToRemoteAssets.ts +++ b/packages/repack/src/loaders/assetsLoader/convertToRemoteAssets.ts @@ -26,8 +26,10 @@ export function convertToRemoteAssets({ .join(assetsDirname, resourceDirname) .replace(pathSeparatorRegexp, '/'); - // works on both unix & windows - const publicPathURL = new URL(path.join(remotePublicPath, assetPath)); + // `remotePublicPath` is a URL, not a filesystem path, so it is joined with + // `path.posix` — `path.join` would rewrite the separators on Windows and + // produce something `new URL` rejects. + const publicPathURL = new URL(path.posix.join(remotePublicPath, assetPath)); const size = getAssetSize(assets); diff --git a/packages/repack/src/loaders/assetsLoader/extractAssets.ts b/packages/repack/src/loaders/assetsLoader/extractAssets.ts index f50d91e80..665182c4d 100644 --- a/packages/repack/src/loaders/assetsLoader/extractAssets.ts +++ b/packages/repack/src/loaders/assetsLoader/extractAssets.ts @@ -36,7 +36,10 @@ export function extractAssets( .replace(pathSeparatorRegexp, '/'); if (customPublicPath) { - publicPath = path.join(customPublicPath, publicPath); + // `publicPath` is served over HTTP and always uses forward slashes, so it + // is joined with `path.posix` regardless of the platform the bundle is + // built on. + publicPath = path.posix.join(customPublicPath, publicPath); } const size = getAssetSize(assets); From c61728fb3c656ab42963c2348b707a866527e582 Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Sat, 26 Sep 2026 14:54:01 +0100 Subject: [PATCH 2/2] test(assets-loader): cover public path joining with Windows path semantics CI does not run on Windows for pull requests, so these tests mock node:path with path.win32 to catch a regression back to path.join on Linux and macOS. --- .../__tests__/windowsPaths.test.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 packages/repack/src/loaders/assetsLoader/__tests__/windowsPaths.test.ts diff --git a/packages/repack/src/loaders/assetsLoader/__tests__/windowsPaths.test.ts b/packages/repack/src/loaders/assetsLoader/__tests__/windowsPaths.test.ts new file mode 100644 index 000000000..2d474228f --- /dev/null +++ b/packages/repack/src/loaders/assetsLoader/__tests__/windowsPaths.test.ts @@ -0,0 +1,71 @@ +import path from 'node:path'; +import { convertToRemoteAssets } from '../convertToRemoteAssets.js'; +import { extractAssets } from '../extractAssets.js'; +import type { Asset } from '../types.js'; + +// Run the loader helpers with Windows path semantics: `path.join` uses +// backslashes, while `path.posix` stays available as it is on Windows. +jest.mock('node:path', () => { + const { win32 } = jest.requireActual('node:path'); + return { __esModule: true, default: win32, ...win32 }; +}); + +const pathSeparatorRegexp = new RegExp(`\\${path.sep}`, 'g'); + +const assets: Asset[] = [ + { data: Buffer.from(''), dimensions: null, filename: 'logo.png', scale: 1 }, +]; + +describe('assets loader public paths on Windows', () => { + it.each([ + [ + 'http://localhost:9999', + 'src\\img', + 'http://localhost:9999/assets/src/img', + ], + [ + 'https://cdn.example.com/assets/', + 'node_modules\\pkg\\img', + 'https://cdn.example.com/assets/assets/node_modules/pkg/img', + ], + ])( + 'builds a valid remote URL from %s', + (remotePublicPath, resourceDirname, expected) => { + const code = convertToRemoteAssets({ + assets, + assetsDirname: 'assets', + remotePublicPath, + resourceDirname, + resourceExtensionType: 'png', + resourceFilename: 'logo', + resourcePath: 'logo.png', + suffixPattern: '', + pathSeparatorRegexp, + }); + + expect(code).toContain(`"httpServerLocation":"${expected}"`); + } + ); + + it.each([ + [undefined, 'assets/src/img'], + ['/public', '/public/assets/src/img'], + ])('joins publicPath %s with forward slashes', (publicPath, expected) => { + const code = extractAssets( + { + resourcePath: 'logo.png', + resourceDirname: 'src\\img', + resourceFilename: 'logo', + resourceExtensionType: 'png', + assets, + suffixPattern: '', + assetsDirname: 'assets', + pathSeparatorRegexp, + publicPath, + }, + { debug: () => {} } + ); + + expect(code).toContain(`httpServerLocation: "${expected}"`); + }); +});