From 79cf69086de0731437035796987b6b849a80a576 Mon Sep 17 00:00:00 2001 From: Jesse Peterman Date: Tue, 10 Feb 2026 11:05:57 -0800 Subject: [PATCH] feat: add Metro 0.83 compatibility layer Metro 0.83 introduced a restrictive exports field that only allows metro/private/* paths instead of direct metro/src/* imports. This adds a metro-compat.ts utility that dynamically resolves the correct import path based on the installed Metro version, ensuring compatibility with both Metro 0.82 and 0.83+. Changes: - Add src/utils/metro-compat.ts compatibility layer - Update all metro/src/* imports to use metro-compat - Handle both .default and direct exports across Metro versions - Add changeset Co-authored-by: Zack Chapple <2133184+zackarychapple@users.noreply.github.com> --- .changeset/metro-083-compat.md | 5 ++ .../src/commands/bundle-host/index.ts | 8 ++- .../src/commands/bundle-remote/index.ts | 12 +++- .../src/commands/utils/create-resolver.ts | 7 +- .../commands/utils/get-community-plugin.ts | 9 ++- .../src/commands/utils/save-bundle-and-map.ts | 6 +- packages/metro-core/src/plugin/serializer.ts | 8 ++- packages/metro-core/src/utils/metro-compat.ts | 66 +++++++++++++++++++ packages/metro-core/src/utils/vm-manager.ts | 16 +++-- 9 files changed, 116 insertions(+), 21 deletions(-) create mode 100644 .changeset/metro-083-compat.md create mode 100644 packages/metro-core/src/utils/metro-compat.ts diff --git a/.changeset/metro-083-compat.md b/.changeset/metro-083-compat.md new file mode 100644 index 00000000000..242776d53d7 --- /dev/null +++ b/.changeset/metro-083-compat.md @@ -0,0 +1,5 @@ +--- +"@module-federation/metro": patch +--- + +Add Metro 0.83 compatibility layer. Metro 0.83 introduced a restrictive `exports` field that only allows `metro/private/*` paths instead of direct `metro/src/*` imports. This adds a `metro-compat` utility that dynamically resolves the correct import path, ensuring compatibility with both Metro 0.82 and 0.83+. diff --git a/packages/metro-core/src/commands/bundle-host/index.ts b/packages/metro-core/src/commands/bundle-host/index.ts index dfe3908d604..59cecd1b3c1 100644 --- a/packages/metro-core/src/commands/bundle-host/index.ts +++ b/packages/metro-core/src/commands/bundle-host/index.ts @@ -1,9 +1,8 @@ import path from 'node:path'; import util from 'node:util'; -import Server from 'metro/src/Server'; -import type { RequestOptions } from 'metro/src/shared/types'; import type { ModuleFederationConfigNormalized } from '../../types'; import { CLIError } from '../../utils/errors'; +import { type RequestOptions, Server } from '../../utils/metro-compat'; import type { Config } from '../types'; import { createResolver } from '../utils/create-resolver'; import { getCommunityCliPlugin } from '../utils/get-community-plugin'; @@ -59,7 +58,10 @@ async function bundleFederatedHost( communityCliPlugin.unstable_buildBundleWithConfig; return buildBundleWithConfig(args, config, { - build: async (server: Server, requestOpts: RequestOptions) => { + build: async ( + server: InstanceType, + requestOpts: RequestOptions, + ) => { // setup enhance middleware to trigger virtual modules setup config.server.enhanceMiddleware(server.processRequest, server); const resolver = await createResolver(server, args.platform); diff --git a/packages/metro-core/src/commands/bundle-remote/index.ts b/packages/metro-core/src/commands/bundle-remote/index.ts index 946eac4eb4a..c0053240315 100644 --- a/packages/metro-core/src/commands/bundle-remote/index.ts +++ b/packages/metro-core/src/commands/bundle-remote/index.ts @@ -3,10 +3,13 @@ import path from 'node:path'; import { pathToFileURL } from 'node:url'; import util from 'node:util'; import { mergeConfig } from 'metro'; -import Server from 'metro/src/Server'; -import type { OutputOptions, RequestOptions } from 'metro/src/shared/types'; import type { ModuleFederationConfigNormalized } from '../../types'; import { CLIError } from '../../utils/errors'; +import { + type OutputOptions, + type RequestOptions, + Server, +} from '../../utils/metro-compat'; import type { Config } from '../types'; import { createModulePathRemapper } from '../utils/create-module-path-remapper'; import { createResolver } from '../utils/create-resolver'; @@ -43,7 +46,10 @@ interface BundleRequestOptions extends RequestOptions { sourceUrl: string; } -async function buildBundle(server: Server, requestOpts: BundleRequestOptions) { +async function buildBundle( + server: InstanceType, + requestOpts: BundleRequestOptions, +) { const bundle = await server.build({ ...Server.DEFAULT_BUNDLE_OPTIONS, ...requestOpts, diff --git a/packages/metro-core/src/commands/utils/create-resolver.ts b/packages/metro-core/src/commands/utils/create-resolver.ts index f12574a2cad..795c37dd01d 100644 --- a/packages/metro-core/src/commands/utils/create-resolver.ts +++ b/packages/metro-core/src/commands/utils/create-resolver.ts @@ -1,4 +1,4 @@ -import type Server from 'metro/src/Server'; +import type { Server } from '../../utils/metro-compat'; /** * Creates a resolver utility that mirrors Metro's bundling resolution behavior. @@ -14,7 +14,10 @@ import type Server from 'metro/src/Server'; * @param platform - The target platform for resolution (e.g., 'ios', 'android', null for default) * @returns The resolver object with a resolve method that takes source and target paths */ -export async function createResolver(server: Server, platform: string | null) { +export async function createResolver( + server: InstanceType, + platform: string | null, +) { const bundler = server.getBundler().getBundler(); const depGraph = await bundler.getDependencyGraph(); diff --git a/packages/metro-core/src/commands/utils/get-community-plugin.ts b/packages/metro-core/src/commands/utils/get-community-plugin.ts index 7a1e73be00a..0281ec4953d 100644 --- a/packages/metro-core/src/commands/utils/get-community-plugin.ts +++ b/packages/metro-core/src/commands/utils/get-community-plugin.ts @@ -1,7 +1,10 @@ import type { ConfigT } from 'metro-config'; -import type Server from 'metro/src/Server'; -import type { OutputOptions, RequestOptions } from 'metro/src/shared/types'; import { CLIError } from '../../utils/errors'; +import type { + OutputOptions, + RequestOptions, + Server, +} from '../../utils/metro-compat'; interface Command { name: string; @@ -21,7 +24,7 @@ interface CommunityCliPlugin { config: ConfigT, bundleImpl: { build: ( - server: Server, + server: InstanceType, requestOpts: RequestOptions, ) => Promise<{ code: string; map: string }>; save: ( diff --git a/packages/metro-core/src/commands/utils/save-bundle-and-map.ts b/packages/metro-core/src/commands/utils/save-bundle-and-map.ts index c059a28bf83..5a47b92e033 100644 --- a/packages/metro-core/src/commands/utils/save-bundle-and-map.ts +++ b/packages/metro-core/src/commands/utils/save-bundle-and-map.ts @@ -1,8 +1,10 @@ import { promises as fs } from 'node:fs'; import util from 'node:util'; import type { MixedSourceMap } from 'metro-source-map'; -import relativizeSourceMapInline from 'metro/src/lib/relativizeSourceMap'; -import type { OutputOptions } from 'metro/src/shared/types'; +import { + type OutputOptions, + relativizeSourceMapInline, +} from '../../utils/metro-compat'; function relativizeSerializedMap( map: string, diff --git a/packages/metro-core/src/plugin/serializer.ts b/packages/metro-core/src/plugin/serializer.ts index a84f94e35df..0037d8d46b1 100644 --- a/packages/metro-core/src/plugin/serializer.ts +++ b/packages/metro-core/src/plugin/serializer.ts @@ -1,11 +1,13 @@ import path from 'node:path'; import type { Module, ReadOnlyGraph, SerializerOptions } from 'metro'; import type { SerializerConfigT } from 'metro-config'; -import baseJSBundle from 'metro/src/DeltaBundler/Serializers/baseJSBundle'; -import CountingSet from 'metro/src/lib/CountingSet'; -import bundleToString from 'metro/src/lib/bundleToString'; import type { ModuleFederationConfigNormalized, Shared } from '../types'; import { ConfigError } from '../utils/errors'; +import { + CountingSet, + baseJSBundle, + bundleToString, +} from '../utils/metro-compat'; type CustomSerializer = SerializerConfigT['customSerializer']; diff --git a/packages/metro-core/src/utils/metro-compat.ts b/packages/metro-core/src/utils/metro-compat.ts new file mode 100644 index 00000000000..ef7dd7acd57 --- /dev/null +++ b/packages/metro-core/src/utils/metro-compat.ts @@ -0,0 +1,66 @@ +/** + * Metro Compatibility Layer + * + * Provides backwards-compatible imports for Metro 0.82 and 0.83+ + * + * Metro 0.83 introduced a restrictive `exports` field that only allows + * `metro/private/*` paths instead of direct `metro/src/*` imports. + * + * This module dynamically resolves the correct import path based on the + * installed Metro version, ensuring compatibility with both versions. + */ + +/** + * Attempts to import from Metro 0.83 format first, falls back to 0.82 format + */ +function tryImport(metro83Path: string, metro82Path: string) { + try { + return require(metro83Path); + } catch { + return require(metro82Path); + } +} + +function getDefaultExport(mod: any) { + // CJS interop: if the module has a .default that is a function or object, use it + // Otherwise return the module itself (already the direct export) + if (mod != null && typeof mod === 'object' && 'default' in mod) { + return mod.default; + } + return mod; +} + +// Server class +export const Server = getDefaultExport( + tryImport('metro/private/Server', 'metro/src/Server'), +); + +// DeltaBundler Serializers +export const baseJSBundle = getDefaultExport( + tryImport( + 'metro/private/DeltaBundler/Serializers/baseJSBundle', + 'metro/src/DeltaBundler/Serializers/baseJSBundle', + ), +); + +// Utility classes +export const CountingSet = getDefaultExport( + tryImport('metro/private/lib/CountingSet', 'metro/src/lib/CountingSet'), +); + +// Bundle utilities +export const bundleToString = getDefaultExport( + tryImport('metro/private/lib/bundleToString', 'metro/src/lib/bundleToString'), +); + +const relativizeSourceMapModule = tryImport( + 'metro/private/lib/relativizeSourceMap', + 'metro/src/lib/relativizeSourceMap', +); +export const relativizeSourceMapInline = + relativizeSourceMapModule.relativizeSourceMapInline || + getDefaultExport(relativizeSourceMapModule); + +// Re-export types - these come from metro/src/shared/types in both versions +// The types themselves are available through the main 'metro' package export +export type { RequestOptions, OutputOptions } from 'metro/src/shared/types'; diff --git a/packages/metro-core/src/utils/vm-manager.ts b/packages/metro-core/src/utils/vm-manager.ts index 7babfca544a..ff4f1738099 100644 --- a/packages/metro-core/src/utils/vm-manager.ts +++ b/packages/metro-core/src/utils/vm-manager.ts @@ -6,11 +6,13 @@ import type { TransformerConfigT, } from 'metro-config'; import type { FileSystem } from 'metro-file-map'; -import type MetroServer from 'metro/src/Server'; +import type { Server as MetroServer } from './metro-compat'; type EnhanceMiddleware = ServerConfigT['enhanceMiddleware']; type GetTransformOptions = TransformerConfigT['getTransformOptions']; -type Bundler = ReturnType['getBundler']>; +type Bundler = ReturnType< + ReturnType['getBundler']>['getBundler'] +>; export class VirtualModuleManager { private setupFinished: Promise | null = null; @@ -69,8 +71,8 @@ export class VirtualModuleManager { setup(bundler: Bundler) { this.setupFinished = (async () => { const graph = await bundler.getDependencyGraph(); - // @ts-expect-error incomplete types - this.ensureFileSystemPatched(graph._fileSystem); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.ensureFileSystemPatched((graph as any)._fileSystem); this.ensureBundlerPatched(bundler); return true; })(); @@ -104,7 +106,11 @@ export class VirtualModuleManager { } const transformFile = bundler.transformFile.bind(bundler); - bundler.transformFile = async (filePath, transformOptions, fileBuffer) => { + bundler.transformFile = async ( + filePath: string, + transformOptions: any, + fileBuffer?: Buffer, + ) => { let buffer = fileBuffer; const virtualModule = this.virtualModules.get(filePath);