From 0f723921229eedf20a6767c65c6bec77d705ddec Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Thu, 23 Jul 2026 19:09:06 +0300 Subject: [PATCH] fix(babel): normalize resolved paths to POSIX so Windows rewrites work The import plugin's relative-import handlers resolve a source against the file being transformed and then match the result against forward-slash literals (`react-native/Libraries/Components/`, `react-native-web/dist`). path.resolve returns backslash-separated paths on Windows, so those splits never match and the import is left un-rewritten. Only relative imports break, and only on Windows -- bare specifiers are plain string matches; the failing cases were green on the Linux/macOS CI. Add a `resolvePosix` helper (resolve + normalize \ -> /) and use it in parseReactNativeSource / parseReactNativeWebSource. Resolve semantics are unchanged; only the separator is normalized. import-plugin's isFromThisModule stays on path.resolve -- it compares two OS-native paths, so it already works. Adds a helpers unit test asserting the POSIX invariant. --- src/__tests__/babel/helpers.test.ts | 20 ++++++++++++++++++++ src/babel/helpers.ts | 16 ++++++++++++++++ src/babel/react-native-web.ts | 5 ++--- src/babel/react-native.ts | 5 +++-- 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/babel/helpers.test.ts diff --git a/src/__tests__/babel/helpers.test.ts b/src/__tests__/babel/helpers.test.ts new file mode 100644 index 00000000..d0520a14 --- /dev/null +++ b/src/__tests__/babel/helpers.test.ts @@ -0,0 +1,20 @@ +import { resolvePosix } from "../../babel/helpers"; + +describe("resolvePosix", () => { + test("resolves to an absolute path with POSIX separators on every platform", () => { + // The invariant the import handlers depend on: no backslashes leak through, + // so their forward-slash `split` / `startsWith` matching works on Windows + // (where path.resolve otherwise yields "\"-separated paths). + const result = resolvePosix(process.cwd(), "a", "b", "c"); + + expect(result).not.toContain("\\"); + expect(result.split("/").slice(-3)).toEqual(["a", "b", "c"]); + }); + + test("collapses '..' segments like path.resolve, keeping POSIX separators", () => { + const result = resolvePosix(process.cwd(), "a", "b", "..", "c"); + + expect(result).not.toContain("\\"); + expect(result.split("/").slice(-2)).toEqual(["a", "c"]); + }); +}); diff --git a/src/babel/helpers.ts b/src/babel/helpers.ts index c66f630c..9a08e5d0 100644 --- a/src/babel/helpers.ts +++ b/src/babel/helpers.ts @@ -1,3 +1,5 @@ +import { resolve } from "path"; + import tBabelTypes, { type CallExpression } from "@babel/types"; export type BabelTypes = typeof tBabelTypes; @@ -38,3 +40,17 @@ export function getInteropRequireDefaultSource( return requireArg.value; } + +/** + * `path.resolve`, normalized to POSIX separators. + * + * The relative-import handlers resolve a source against the file being + * transformed and then match the result against forward-slash literals + * (`react-native/Libraries/Components/`, `react-native-web/dist`, …). On Windows + * `path.resolve` yields backslash separators, so those `split` / `startsWith` + * matches silently miss and the import is left un-rewritten. Normalizing to `/` + * makes the matching platform-independent. + */ +export function resolvePosix(...segments: string[]): string { + return resolve(...segments).replace(/\\/g, "/"); +} diff --git a/src/babel/react-native-web.ts b/src/babel/react-native-web.ts index 1bacf5a0..db860a49 100644 --- a/src/babel/react-native-web.ts +++ b/src/babel/react-native-web.ts @@ -1,5 +1,3 @@ -import { resolve } from "path"; - import { type NodePath } from "@babel/traverse"; import tBabelTypes, { type ImportDeclaration, @@ -9,12 +7,13 @@ import tBabelTypes, { } from "@babel/types"; import { allowedModules } from "./allowedModules"; +import { resolvePosix } from "./helpers"; type BabelTypes = typeof tBabelTypes; function parseReactNativeWebSource(source: string, filename: string) { if (source.startsWith(".")) { - source = resolve(filename, source); + source = resolvePosix(filename, source); const internalPath = source.split("react-native-web/dist")[1]; if (!internalPath) { diff --git a/src/babel/react-native.ts b/src/babel/react-native.ts index 2522a848..ad65c941 100644 --- a/src/babel/react-native.ts +++ b/src/babel/react-native.ts @@ -1,4 +1,4 @@ -import { dirname, resolve } from "path"; +import { dirname } from "path"; import { type NodePath } from "@babel/traverse"; import tBabelTypes, { @@ -9,12 +9,13 @@ import tBabelTypes, { } from "@babel/types"; import { allowedModules } from "./allowedModules"; +import { resolvePosix } from "./helpers"; type BabelTypes = typeof tBabelTypes; function parseReactNativeSource(source: string, filename: string) { if (source.startsWith(".")) { - source = resolve(dirname(filename), source); + source = resolvePosix(dirname(filename), source); const internalPath = source.split("react-native/Libraries/Components/")[1]; if (!internalPath) {