diff --git a/.changeset/metro-windows-path-comparisons.md b/.changeset/metro-windows-path-comparisons.md new file mode 100644 index 00000000000..99fe5c0adcd --- /dev/null +++ b/.changeset/metro-windows-path-comparisons.md @@ -0,0 +1,5 @@ +--- +'@module-federation/metro': patch +--- + +fix(metro): normalize remaining Windows path comparisons in the resolver and babel plugin. The init-host/remote-entry origin-module checks, the patched HMRClient check, and the babel plugin's `blacklistedPaths`/`state.filename` check now compare POSIX-normalized paths so they match correctly on Windows, where Metro and `path.resolve` can disagree on path separators. diff --git a/packages/metro-core/__tests__/plugin/resolver.spec.ts b/packages/metro-core/__tests__/plugin/resolver.spec.ts index 37f2bc6938c..7f6aaf27110 100644 --- a/packages/metro-core/__tests__/plugin/resolver.spec.ts +++ b/packages/metro-core/__tests__/plugin/resolver.spec.ts @@ -168,4 +168,46 @@ describe('createResolveRequest', () => { expect(fallbackResolver).not.toHaveBeenCalled(); }, ); + + it('matches the init-host origin module path regardless of separator (Windows)', () => { + // Metro and `path.resolve` can disagree on path separators on Windows. + // The generated init-host path uses native (backslash) separators while + // the origin module path reported by Metro may use POSIX separators. + const projectDir = 'C:\\project'; + const tmpDir = 'C:\\project\\node_modules\\.mf-metro'; + const initHost = 'C:\\project\\node_modules\\.mf-metro\\init-host.js'; + const config = createConfig('lodash', 'lodash'); + const fallbackResolver = vi.fn(() => ({ + type: 'sourceFile', + filePath: '/fallback.js', + })); + // Metro reports the same file using POSIX separators + const context = createResolverContext( + 'C:/project/node_modules/.mf-metro/init-host.js', + fallbackResolver, + ); + const vmManager: Pick = { + registerVirtualModule: vi.fn(), + }; + + const resolveRequest = createResolveRequest({ + isRemote: false, + hacks: { patchHMRClient: false, patchInitializeCore: false }, + options: config, + paths: { + ...createPaths(projectDir, tmpDir), + initHost, + }, + vmManager, + }); + + const resolved = resolveRequest(context, 'lodash', 'ios'); + + // init-host imports its shared deps directly, so the request should fall + // through to the host resolver instead of being rewritten to a virtual + // shared module. + expect(resolved).toEqual({ type: 'sourceFile', filePath: '/fallback.js' }); + expect(fallbackResolver).toHaveBeenCalledTimes(1); + expect(vmManager.registerVirtualModule).not.toHaveBeenCalled(); + }); }); diff --git a/packages/metro-core/babel-plugin/index.js b/packages/metro-core/babel-plugin/index.js index 2581c2236b1..73d9b2f153f 100644 --- a/packages/metro-core/babel-plugin/index.js +++ b/packages/metro-core/babel-plugin/index.js @@ -80,12 +80,23 @@ function getRejectedPromise(errorMessage) { ); } +// normalize paths to POSIX separators so comparisons against generated +// paths work on Windows, where Metro and `path.resolve` can disagree +// on path separators +function toPosixPath(value) { + return typeof value === 'string' ? value.replace(/\\/g, '/') : value; +} + function moduleFederationMetroBabelPlugin() { return { name: 'module-federation-metro-babel-plugin', visitor: { CallExpression(path, state) { - if (state.opts.blacklistedPaths.includes(state.filename)) { + const filename = toPosixPath(state.filename); + const blacklistedPaths = (state.opts.blacklistedPaths || []).map( + toPosixPath, + ); + if (blacklistedPaths.includes(filename)) { return; } diff --git a/packages/metro-core/src/plugin/resolver.ts b/packages/metro-core/src/plugin/resolver.ts index 42faf51204e..bdfcefc0fad 100644 --- a/packages/metro-core/src/plugin/resolver.ts +++ b/packages/metro-core/src/plugin/resolver.ts @@ -64,6 +64,11 @@ export function createResolveRequest({ }); return function resolveRequest(context, moduleName, platform) { + // normalize the origin module path so comparisons against generated + // paths work on Windows, where Metro and `path.resolve` can disagree + // on path separators + const originModulePath = toPosixPath(context.originModulePath); + // virtual entrypoint for host if (moduleName.match(hostEntryPathRegex)) { const hostEntryGenerator = () => @@ -119,7 +124,7 @@ export function createResolveRequest({ } // shared modules handling in init-host.js - if ([paths.initHost].includes(context.originModulePath)) { + if (toPosixPath(paths.initHost) === originModulePath) { // init-host contains definition of shared modules so we need to prevent // circular import of shared module, by allowing import shared dependencies directly return customResolver @@ -128,7 +133,7 @@ export function createResolveRequest({ } // shared modules handling in remote-entry.js - if ([paths.remoteEntry].includes(context.originModulePath)) { + if (toPosixPath(paths.remoteEntry) === originModulePath) { const sharedModule = options.shared[moduleName]; // import: false means that the module is marked as external if (sharedModule && sharedModule.import === false) { @@ -192,7 +197,7 @@ export function createResolveRequest({ if ( hacks.patchHMRClient && moduleName.endsWith('HMRClient') && - context.originModulePath !== resolveModule('HMRClient.ts') + originModulePath !== toPosixPath(resolveModule('HMRClient.ts')) ) { const res = customResolver ? customResolver(context, moduleName, platform)