// native-seam manifest — the canonical registry of upstream packages whose // native-module spec files we override. // // the rule: when a guest bundle imports from one of these packages, vite lets // the package's JS resolve from node_modules normally. only the listed // `seamBasenames` are redirected to our stub. this keeps the upstream JS layer // (hooks, helpers, transforms, listeners) running unchanged. // // adding a new entry: // 1. identify upstream's native-module file (look for files calling // `requireNativeModule`, `TurboModuleRegistry.getEnforced`, // `requireNativeComponent`, or in `src/specs/Native*Module.ts`) // 2. write a thin stub at `packages/compat/src/stubs/native-seams/.ts` // that exports the same default + named exports upstream's seam file // exports — except the implementation calls into sootsim's engine. // 3. append an entry below. the plugin handles the rest. // // canonical example: react-native-reanimated. see // - `packages/compat/src/stubs/react-native-reanimated.ts` (the seam stub) // - `reanimatedNativeSeamRedirect` in vite-plugin.ts (still hard-coded // because it has package-specific edge cases — fabricUtils, // validate-worklets-version. new entries should use this manifest path // for the simple basename-match case.) export interface NativeSeamEntry { /** * upstream npm package name. all imports whose importer path includes * `//` are candidates for redirect. */ pkg: string /** * basenames of upstream files to redirect. each entry matches the file * name (no extension, no platform suffix like `.ios` / `.android`) of any * upstream module imported transitively from within `pkg`. * * example: if upstream has * `node_modules/expo-notifications/src/NotificationsHandler.ts`, list * `'NotificationsHandler'` here and we'll redirect any import resolving * to that file to our seam stub. */ seamBasenames: string[] /** * absolute path to the seam stub. typically * `packages/compat/src/stubs/native-seams/.ts` — but a single stub * file may export every native module the package exposes, so the same * `target` can appear in multiple entries if a package has multiple native * modules that share an implementation file. * * authors set this via the `seamPath()` helper below. */ target: string /** * notes — short reason this entry exists. shown by the debug logger when * `SOOTSIM_NATIVE_SEAM_DEBUG=1` is set. */ notes?: string } import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const HERE = path.dirname(fileURLToPath(import.meta.url)) const NATIVE_SEAMS_DIR = path.resolve(HERE, '../../compat/src/stubs/native-seams') /** resolve a stub path under `packages/compat/src/stubs/native-seams/`. * resolves to `.tsx` when only that exists (JSX-bearing seams like * expo-image, react-native-webview). preferred order: `.ts`, `.tsx`. */ export function seamPath(basename: string): string { const ts = path.resolve(NATIVE_SEAMS_DIR, `${basename}.ts`) if (fs.existsSync(ts)) return ts const tsx = path.resolve(NATIVE_SEAMS_DIR, `${basename}.tsx`) if (fs.existsSync(tsx)) return tsx // fall through to .ts so vite's "Could not load" error names the .ts // path the author probably intended to create, instead of silently // pointing at something that doesn't exist with no extension. return ts } /** * the manifest. agents append entries here during the native-seam migration * (plans/sootsim/compat/compat-stubs-native-seam-audit.md). keep alphabetical by `pkg` to * minimise merge conflicts. */ export const NATIVE_SEAM_MANIFEST: NativeSeamEntry[] = [ // entries land here during the Phase 1 migration. example shape: // // { // pkg: 'expo-notifications', // seamBasenames: [ // 'NotificationsHandler', // 'NotificationsEmitter', // 'NotificationScheduler', // 'NotificationCategoriesService', // 'ServerRegistrationModule', // ], // target: seamPath('expo-notifications'), // notes: 'route through engine NotificationBus', // }, { pkg: '@metamask/react-native-search-api', // upstream is single-file `index.js`; no sub-imports inside the package // to redirect at the basename layer. listed here for documentation parity // — the bundle-loader's stub-registry routes the package-entry directly to // `stubs/metamask-react-native-search-api.ts`, which pairs the upstream // JS wrapper with the native seam below. seamBasenames: ['index'], target: seamPath('metamask-react-native-search-api'), notes: 'iOS CoreSpotlight + NSUserActivity native seam; pure-JS wrapper class lives at stubs/metamask-react-native-search-api.ts', }, { pkg: '@ua/react-native-airship', seamBasenames: [ // src/NativeRNAirship.ts — the RNAirship TurboModule 'NativeRNAirship', // codegen host components (no native backing in sootsim) 'RNAirshipEmbeddedViewNativeComponent', 'RNAirshipMessageViewNativeComponent', ], target: seamPath('react-native-airship'), notes: 'push-notification SAAS — noop the SAAS layer, route banners through NotificationBus', }, { pkg: 'react-native-background-fetch', // src/NativeBackgroundFetch.ts — TurboModuleRegistry.getEnforcing('RNBackgroundFetch') seamBasenames: ['NativeBackgroundFetch'], target: seamPath('react-native-background-fetch'), notes: 'iOS BGTaskScheduler / Android WorkManager TurboModule — no real scheduler in browser; acknowledge registrations, never fire periodic events', }, { pkg: 'expo-apple-authentication', // src/ExpoAppleAuthenticationButton.ts is the package's native-view // boundary. direct-compiled apps do not run the fetched-bundle lazy seam // loader, so route that boundary to the same view manager implementation. seamBasenames: ['ExpoAppleAuthenticationButton'], target: seamPath('expo-apple-authentication-view'), notes: 'ASAuthorizationAppleIDButton native view — render the system button surface while upstream AppleAuthenticationButton.tsx remains unchanged', }, { pkg: 'expo-asset', // src/ExpoAsset.ts — `requireNativeModule('ExpoAsset')`; only // `downloadAsync` is on the native surface. all the rest (`Asset`, // `AssetHooks`, `AssetSourceResolver`, ...) is pure JS that composes on // top. seamBasenames: ['ExpoAsset'], target: seamPath('expo-asset'), notes: 'expo-modules-core requireNativeModule(ExpoAsset) — only downloadAsync; mirror upstream web build (return remote URL)', }, { pkg: '@bam.tech/react-native-image-resizer', // upstream is not shipped to most guest bundles via node_modules — it // arrives through the bundle-loader stub-registry which routes the // package entry to `stubs/bam-react-native-image-resizer.ts`. listed here // for documentation parity; vite-path redirect would key on // `NativeImageResizer`. seamBasenames: ['NativeImageResizer'], target: seamPath('bam-react-native-image-resizer'), notes: 'ImageResizer TurboModule — browser-side canvas resize implementing the iOS contain/cover/stretch + onlyScaleDown semantics from ios/ImageResizer.mm', }, { pkg: 'react-native-adjust', // upstream's `index.js` reads `NativeModules.Adjust` and // `NativeModules.AdjustEventEmitter` directly — there is no NativeAdjust // spec file. listed for documentation parity; the actual seam delivery is // through the bundle-loader stub-registry routing the package entry to // `stubs/react-native-adjust.ts`, which imports the native seam from this // target. seamBasenames: ['index'], target: seamPath('react-native-adjust'), notes: 'Adjust attribution SAAS — noop native side (Adjust + AdjustEventEmitter NativeModules), pure-JS classes (AdjustConfig/AdjustEvent/...) live in stubs/react-native-adjust.ts', }, { pkg: 'expo-clipboard', // src/ExpoClipboard.ts — `requireNativeModule('ExpoClipboard')`. // upstream also ships `ExpoClipboard.web.ts` (which we mirror); the // redirect catches both `.ts` and `.web.ts` since basenames match. seamBasenames: ['ExpoClipboard'], target: seamPath('expo-clipboard'), notes: 'expo-modules-core requireNativeModule(ExpoClipboard) — real navigator.clipboard text + ClipboardItem image; mirrors upstream ExpoClipboard.web.ts', }, { pkg: 'expo-font', // src/ExpoFontLoader.ts — `requireNativeModule('ExpoFontLoader')`. // we mirror upstream's web variant `ExpoFontLoader.web.ts` (FontFace API // + managed