rnxsim 0.0.0 → 0.1.313
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +512 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +39 -0
- package/detox/expectations.ts +537 -0
- package/detox/gestures.ts +480 -0
- package/detox/index.ts +2118 -0
- package/detox/jest-preset.cjs +51 -0
- package/detox/jest-setup-after-env.cjs +18 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation.ts +43 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-cli/bin.js +15 -0
- package/dist-cli/chunks/agent-XZ2KTPCU.js +48 -0
- package/dist-cli/chunks/agent-wrapper-JJYYW2WH.js +17 -0
- package/dist-cli/chunks/app-fonts-IXRNQG6B.js +4 -0
- package/dist-cli/chunks/assert-54T5SK5F.js +49 -0
- package/dist-cli/chunks/auth-FI5UDI45.js +12 -0
- package/dist-cli/chunks/beta-JV6UKADW.js +4 -0
- package/dist-cli/chunks/camera-VL73YIKP.js +22 -0
- package/dist-cli/chunks/chunk-2D2UPBBR.js +4 -0
- package/dist-cli/chunks/chunk-2YR5BGA5.js +5 -0
- package/dist-cli/chunks/chunk-3NV2NCNX.js +4 -0
- package/dist-cli/chunks/chunk-46EUUFJ5.js +4 -0
- package/dist-cli/chunks/chunk-4NPPOV2N.js +5 -0
- package/dist-cli/chunks/chunk-5DHC6KHQ.js +7 -0
- package/dist-cli/chunks/chunk-5TEF3ET3.js +74 -0
- package/dist-cli/chunks/chunk-5TPRP5QT.js +4 -0
- package/dist-cli/chunks/chunk-5YJCOWCH.js +4 -0
- package/dist-cli/chunks/chunk-7GN3LVWB.js +4 -0
- package/dist-cli/chunks/chunk-BBULZ7CG.js +568 -0
- package/dist-cli/chunks/chunk-BTWORNNG.js +71 -0
- package/dist-cli/chunks/chunk-D4FFVGI5.js +5 -0
- package/dist-cli/chunks/chunk-DCEMHR2Y.js +4 -0
- package/dist-cli/chunks/chunk-DZS6WPUI.js +4 -0
- package/dist-cli/chunks/chunk-F5ZRSS3C.js +3 -0
- package/dist-cli/chunks/chunk-FSUYIVJ6.js +9 -0
- package/dist-cli/chunks/chunk-G2WW6L2C.js +23 -0
- package/dist-cli/chunks/chunk-GASE6UBA.js +4 -0
- package/dist-cli/chunks/chunk-GGRX24GF.js +142 -0
- package/dist-cli/chunks/chunk-HI5TFJWN.js +5 -0
- package/dist-cli/chunks/chunk-IJ5CAZZC.js +5 -0
- package/dist-cli/chunks/chunk-IJO63TDP.js +4 -0
- package/dist-cli/chunks/chunk-KTHV3RUS.js +26 -0
- package/dist-cli/chunks/chunk-LF2ZVT7O.js +6 -0
- package/dist-cli/chunks/chunk-MJRLLB4R.js +38 -0
- package/dist-cli/chunks/chunk-NFK7T35W.js +4 -0
- package/dist-cli/chunks/chunk-NMF2ZMZQ.js +15 -0
- package/dist-cli/chunks/chunk-OHAZNXLK.js +4 -0
- package/dist-cli/chunks/chunk-OVFJFXUD.js +101 -0
- package/dist-cli/chunks/chunk-OZSSI4WN.js +28 -0
- package/dist-cli/chunks/chunk-QKDWYITG.js +25 -0
- package/dist-cli/chunks/chunk-RSZWCKNT.js +30 -0
- package/dist-cli/chunks/chunk-RTN5C5RL.js +4 -0
- package/dist-cli/chunks/chunk-TIVZIMMW.js +4 -0
- package/dist-cli/chunks/chunk-TZFFR3SD.js +7 -0
- package/dist-cli/chunks/chunk-UC6U3MML.js +5 -0
- package/dist-cli/chunks/chunk-VFCMSYZK.js +6 -0
- package/dist-cli/chunks/chunk-VNQEB4L7.js +5 -0
- package/dist-cli/chunks/chunk-VZXWHRUZ.js +983 -0
- package/dist-cli/chunks/chunk-W6K4EFPH.js +107 -0
- package/dist-cli/chunks/chunk-WEXDAC74.js +4 -0
- package/dist-cli/chunks/chunk-WF3T4SVI.js +14 -0
- package/dist-cli/chunks/chunk-WINYQ44O.js +4 -0
- package/dist-cli/chunks/chunk-WMIIKMGK.js +10 -0
- package/dist-cli/chunks/chunk-WUSWBCWA.js +507 -0
- package/dist-cli/chunks/chunk-WWZIXIRD.js +4 -0
- package/dist-cli/chunks/chunk-XEVZYVIW.js +766 -0
- package/dist-cli/chunks/chunk-YDGQTMQL.js +4 -0
- package/dist-cli/chunks/chunk-YIFT42WN.js +4 -0
- package/dist-cli/chunks/chunk-ZMJD5GEC.js +125 -0
- package/dist-cli/chunks/cleanup-P27PA6JI.js +25 -0
- package/dist-cli/chunks/cli-version-WWLPBDQ7.js +4 -0
- package/dist-cli/chunks/compat-ZD65FED3.js +20 -0
- package/dist-cli/chunks/config-XMJRNM2A.js +13 -0
- package/dist-cli/chunks/control-KMIQT3QP.js +4 -0
- package/dist-cli/chunks/daemon-G2ME7NLB.js +4 -0
- package/dist-cli/chunks/debug-PT4HOP7N.js +200 -0
- package/dist-cli/chunks/desktop-S3FG72AK.js +31 -0
- package/dist-cli/chunks/detox-B3D4IFCN.js +52 -0
- package/dist-cli/chunks/device-XBNDSB2R.js +18 -0
- package/dist-cli/chunks/diagnose-HMQXJE5N.js +43 -0
- package/dist-cli/chunks/disk-cleanup-BLCZ5BSZ.js +4 -0
- package/dist-cli/chunks/drivers-LDECZGP2.js +4 -0
- package/dist-cli/chunks/film-BJGTBYZB.js +74 -0
- package/dist-cli/chunks/flow-UEQNVTU7.js +4 -0
- package/dist-cli/chunks/help-T5FYSVGB.js +4 -0
- package/dist-cli/chunks/hidden-runtime-alias-ANOYADHM.js +4 -0
- package/dist-cli/chunks/home-paths-GT3LFNOR.js +4 -0
- package/dist-cli/chunks/inspect-ZA6XF5LD.js +4 -0
- package/dist-cli/chunks/install-desktop-TIMUDHPL.js +4 -0
- package/dist-cli/chunks/login-FJ737MWG.js +28 -0
- package/dist-cli/chunks/logout-ZCNMMHMY.js +4 -0
- package/dist-cli/chunks/maestro-SZTNKLDF.js +104 -0
- package/dist-cli/chunks/maestro-generate-DCFAIZ4H.js +36 -0
- package/dist-cli/chunks/mode-GRMQCRXR.js +26 -0
- package/dist-cli/chunks/optional-demo-registry-W36EWFFB.js +4 -0
- package/dist-cli/chunks/perf-QYBAAUZG.js +48 -0
- package/dist-cli/chunks/permissions-3QCQ6VF4.js +13 -0
- package/dist-cli/chunks/record-QPPC2S4E.js +72 -0
- package/dist-cli/chunks/report-issue-7NMFP4HK.js +29 -0
- package/dist-cli/chunks/runtime-XOAXMSTU.js +4 -0
- package/dist-cli/chunks/screenshot-command-67AECJFB.js +159 -0
- package/dist-cli/chunks/screenshot-layers-ASWBYPJL.js +20 -0
- package/dist-cli/chunks/screenshots-capture-PXA3HFQK.js +42 -0
- package/dist-cli/chunks/serve-BI2NBAXG.js +44 -0
- package/dist-cli/chunks/setup-7DWPMRSB.js +32 -0
- package/dist-cli/chunks/skills-S3Y22TUA.js +50 -0
- package/dist-cli/chunks/storage-XUIMJWAJ.js +37 -0
- package/dist-cli/chunks/store-JTHEJLAZ.js +4 -0
- package/dist-cli/chunks/telemetry-ZYJGD2DB.js +4 -0
- package/dist-cli/chunks/timeline-TMPLQPSP.js +24 -0
- package/dist-cli/chunks/upgrade-7HDSIM7K.js +7 -0
- package/dist-cli/chunks/upload-GMSZPWM6.js +4 -0
- package/dist-cli/chunks/version-HOCHZ37L.js +6 -0
- package/dist-cli/chunks/web-DG3WBYD3.js +4 -0
- package/dist-cli/chunks/what-happened-XFVUTZR7.js +25 -0
- package/dist-lib/agent-daemon-client.cjs +468 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +725 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +195 -0
- package/dist-lib/backend-origin.cjs +70 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +2863 -0
- package/dist-lib/detox/jest-preset.cjs +51 -0
- package/dist-lib/dev-bundle-resolution.cjs +223 -0
- package/dist-lib/home-paths.cjs +421 -0
- package/dist-lib/host/bridge-host.cjs +5600 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +373 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/websocket-proxy.cjs +214 -0
- package/dist-lib/index.cjs +513 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/menu.cjs +620 -0
- package/dist-lib/menu.mjs +581 -0
- package/dist-lib/metro.cjs +297 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
- package/dist-lib/sdk.cjs +2333 -0
- package/dist-lib/sdk.mjs +2240 -0
- package/dist-lib/skills.cjs +23922 -0
- package/dist-lib/vite.cjs +358 -0
- package/package.json +189 -18
- package/scripts/dev-server-scanner.ts +941 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/scripts/postinstall.cjs +3 -0
- package/scripts/preuninstall.cjs +118 -0
- package/skills/contrast/SKILL.md +171 -0
- package/skills/rnx-debug/SKILL.md +425 -0
- package/skills/rnx-perf/SKILL.md +295 -0
- package/skills/rnx-setup/SKILL.md +160 -0
- package/skills/rnx-test/SKILL.md +292 -0
- package/skills/rnx-visual/SKILL.md +122 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +573 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +204 -0
- package/src/backend-origin.ts +49 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +171 -0
- package/src/browser-cache.ts +16 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +32 -0
- package/src/codex-client.ts +215 -0
- package/src/config.ts +134 -0
- package/src/dev-bundle-resolution.ts +236 -0
- package/src/disk-cleanup.ts +466 -0
- package/src/home-paths.ts +552 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2626 -0
- package/src/host/fetch-proxy-handler.ts +397 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/websocket-proxy.ts +210 -0
- package/src/index.ts +12 -705
- package/src/jump-to-source-babel.ts +135 -0
- package/src/menu.ts +734 -0
- package/src/metro-plugin.ts +139 -0
- package/src/native-dev-bundle-url.ts +96 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +92 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/render-mode.ts +27 -0
- package/src/runtime-assets.ts +228 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +454 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +7 -0
- package/src/settings-bundle.ts +418 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +190 -0
- package/src/vite-plugin.ts +1750 -0
- package/src/worklets-babel.ts +134 -0
|
@@ -0,0 +1,1750 @@
|
|
|
1
|
+
// sootsim vite plugin — use in any vite project to get react-native resolution,
|
|
2
|
+
// native dep stubbing, and external app support.
|
|
3
|
+
//
|
|
4
|
+
// usage:
|
|
5
|
+
// import { sootsim } from 'sootsim/vite'
|
|
6
|
+
// export default defineConfig({ plugins: [sootsim()] })
|
|
7
|
+
//
|
|
8
|
+
// // with external app:
|
|
9
|
+
// export default defineConfig({ plugins: [sootsim({ app: '~/my-rn-app' })] })
|
|
10
|
+
|
|
11
|
+
import fs from 'fs'
|
|
12
|
+
import { createRequire } from 'module'
|
|
13
|
+
import path from 'path'
|
|
14
|
+
import { fileURLToPath } from 'url'
|
|
15
|
+
import { type Plugin, transformWithOxc } from 'vite'
|
|
16
|
+
import { resolveExportsHiddenSubpath } from '../../compat/src/resolve-hidden-subpath.ts'
|
|
17
|
+
import {
|
|
18
|
+
SOOTSIM_COMPAT_PUBLISHED_BROWSER_ENTRIES,
|
|
19
|
+
SOOTSIM_COMPAT_WRAPPER_REAL_PACKAGES,
|
|
20
|
+
SOOTSIM_HAPTIC_FEEDBACK_TOUCHABLE_SOURCE,
|
|
21
|
+
SOOTSIM_HAPTIC_FEEDBACK_TOUCHABLE_SPECIFIER,
|
|
22
|
+
compatStubsForBuildResolver,
|
|
23
|
+
reactNativeDeepStubsForBuildResolver,
|
|
24
|
+
} from '../../compat/src/stub-manifest.ts'
|
|
25
|
+
import { resolveSootsimBridgePort } from './bridge-constants.ts'
|
|
26
|
+
import {
|
|
27
|
+
REANIMATED_WORKLETIZATION_REGEX,
|
|
28
|
+
shouldApplyWorkletsPlugin,
|
|
29
|
+
transformWorkletsCode,
|
|
30
|
+
} from './worklets-babel.ts'
|
|
31
|
+
|
|
32
|
+
const sootsimPluginRequire = createRequire(import.meta.url)
|
|
33
|
+
const virtualIdPrefix = String.fromCharCode(0)
|
|
34
|
+
|
|
35
|
+
export interface SootSimOptions {
|
|
36
|
+
// directory of an external RN app to load
|
|
37
|
+
app?: string
|
|
38
|
+
// additional source directories to treat as app sources (metro transforms apply)
|
|
39
|
+
sources?: string[]
|
|
40
|
+
// additional packages that must resolve from the host project, not the external app
|
|
41
|
+
ownedPackages?: string[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// derive sootsim's root from this file's location (src/vite-plugin.ts → ..)
|
|
45
|
+
const sootsimRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
46
|
+
const workspaceRoot = path.resolve(sootsimRoot, '..', '..')
|
|
47
|
+
const workspaceNodeModules = path.resolve(workspaceRoot, 'node_modules')
|
|
48
|
+
const workspaceTamaguiDir = path.resolve(workspaceNodeModules, '@tamagui')
|
|
49
|
+
// stubs and the mutable React bridge live in @sootsim/compat.
|
|
50
|
+
const compatRoot = path.resolve(sootsimRoot, '..', 'compat')
|
|
51
|
+
|
|
52
|
+
// the rendering engine and its shims live in the private sootsim-engine
|
|
53
|
+
// workspace package. resolve via workspace root rather than relative hops so
|
|
54
|
+
// renames of either package don't break things.
|
|
55
|
+
const engineRoot = path.resolve(workspaceRoot, 'packages/sootsim-engine')
|
|
56
|
+
const rnShimPath = path.resolve(engineRoot, 'src/react-native/index.ts')
|
|
57
|
+
const reactBridgePath = path.resolve(compatRoot, 'src/react-bridge.ts')
|
|
58
|
+
const sootsimBrowserSourceAliases = [
|
|
59
|
+
{
|
|
60
|
+
find: 'rnxsim/backend-origin',
|
|
61
|
+
replacement: path.resolve(sootsimRoot, 'src/backend-origin.ts'),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
find: 'rnxsim/bridge-constants',
|
|
65
|
+
replacement: path.resolve(sootsimRoot, 'src/bridge-constants.ts'),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
find: 'rnxsim/dev-bundle-resolution',
|
|
69
|
+
replacement: path.resolve(sootsimRoot, 'src/dev-bundle-resolution.ts'),
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
const compatStubsDir = path.resolve(compatRoot, 'src/stubs')
|
|
74
|
+
const nativeAutoStubPath = path.resolve(compatStubsDir, 'native-auto-stub.ts')
|
|
75
|
+
const rnDeepStubDefault = path.resolve(compatStubsDir, 'react-native-internals.ts')
|
|
76
|
+
|
|
77
|
+
function getInternalTamaguiPackages(): string[] {
|
|
78
|
+
try {
|
|
79
|
+
const scoped = fs
|
|
80
|
+
.readdirSync(workspaceTamaguiDir, { withFileTypes: true })
|
|
81
|
+
.filter((entry) => entry.isDirectory())
|
|
82
|
+
.map((entry) => `@tamagui/${entry.name}`)
|
|
83
|
+
|
|
84
|
+
return ['tamagui', ...scoped].sort()
|
|
85
|
+
} catch {
|
|
86
|
+
return ['tamagui']
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const internalTamaguiPackages = getInternalTamaguiPackages()
|
|
91
|
+
|
|
92
|
+
const vitePackageStubs = compatStubsForBuildResolver('vite')
|
|
93
|
+
const viteReactNativeDeepStubs = reactNativeDeepStubsForBuildResolver('vite')
|
|
94
|
+
const builtinStubs = Object.fromEntries(
|
|
95
|
+
vitePackageStubs.map((entry) => [entry.specifier, entry.stubFile]),
|
|
96
|
+
)
|
|
97
|
+
const rnLibraryStubs = Object.fromEntries(
|
|
98
|
+
viteReactNativeDeepStubs.map((entry) => [
|
|
99
|
+
entry.specifier,
|
|
100
|
+
path.resolve(compatStubsDir, entry.stubFile),
|
|
101
|
+
]),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
type PublishedBrowserPackage = {
|
|
105
|
+
specifier: string
|
|
106
|
+
packageDir: string
|
|
107
|
+
browserEntry: string
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolvePublishedBrowserPackages(appDir: string): PublishedBrowserPackage[] {
|
|
111
|
+
const packageRequire = appDir
|
|
112
|
+
? createRequire(path.join(appDir, 'package.json'))
|
|
113
|
+
: sootsimPluginRequire
|
|
114
|
+
return Object.values(SOOTSIM_COMPAT_PUBLISHED_BROWSER_ENTRIES).flatMap(
|
|
115
|
+
({ specifier }) => {
|
|
116
|
+
try {
|
|
117
|
+
const packageJson = packageRequire.resolve(`${specifier}/package.json`)
|
|
118
|
+
const packageManifest = JSON.parse(fs.readFileSync(packageJson, 'utf8'))
|
|
119
|
+
const rootExport = Reflect.get(packageManifest.exports, '.')
|
|
120
|
+
const browserFile =
|
|
121
|
+
typeof rootExport === 'object' && rootExport !== null
|
|
122
|
+
? Reflect.get(rootExport, 'browser')
|
|
123
|
+
: undefined
|
|
124
|
+
if (typeof browserFile !== 'string') return []
|
|
125
|
+
const packageDir = path.dirname(packageJson)
|
|
126
|
+
const browserEntry = path.resolve(packageDir, browserFile)
|
|
127
|
+
if (!fs.existsSync(browserEntry)) return []
|
|
128
|
+
return [{ specifier, packageDir, browserEntry }]
|
|
129
|
+
} catch {
|
|
130
|
+
return []
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resolvePublishedBrowserInternal(
|
|
137
|
+
source: string,
|
|
138
|
+
importer: string | undefined,
|
|
139
|
+
packages: PublishedBrowserPackage[],
|
|
140
|
+
): string | null {
|
|
141
|
+
if (!importer || !source.startsWith('.')) return null
|
|
142
|
+
const cleanImporter = importer.split('?')[0]
|
|
143
|
+
const packageEntry = packages.find(({ packageDir }) => {
|
|
144
|
+
const relative = path.relative(packageDir, cleanImporter)
|
|
145
|
+
return relative.length > 0 && !relative.startsWith('..') && !path.isAbsolute(relative)
|
|
146
|
+
})
|
|
147
|
+
if (!packageEntry) return null
|
|
148
|
+
|
|
149
|
+
const cleanSource = source.split('?')[0]
|
|
150
|
+
const resolved = path.resolve(path.dirname(cleanImporter), cleanSource)
|
|
151
|
+
const sourceExtension = path.extname(cleanSource)
|
|
152
|
+
if (sourceExtension) {
|
|
153
|
+
const candidate = `${resolved.slice(0, -sourceExtension.length)}.web${sourceExtension}`
|
|
154
|
+
return fs.existsSync(candidate) ? candidate : null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const extension of ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs']) {
|
|
158
|
+
const fileCandidate = `${resolved}.web${extension}`
|
|
159
|
+
if (fs.existsSync(fileCandidate)) return fileCandidate
|
|
160
|
+
const indexCandidate = path.join(resolved, `index.web${extension}`)
|
|
161
|
+
if (fs.existsSync(indexCandidate)) return indexCandidate
|
|
162
|
+
}
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// resolve the real @react-navigation/native location at config time so the
|
|
167
|
+
// wrapper at compat/src/stubs/react-navigation-native.tsx can import it
|
|
168
|
+
// without bouncing back through the alias above. the wrapper imports from
|
|
169
|
+
// the synthetic specifier `@sootsim-internal/react-navigation-native-real`
|
|
170
|
+
// which is aliased to this absolute path; rolldown follows the path
|
|
171
|
+
// terminally so its `export *` static analysis sees the real export
|
|
172
|
+
// surface.
|
|
173
|
+
const reactNavigationNativeRealPath = (() => {
|
|
174
|
+
try {
|
|
175
|
+
return sootsimPluginRequire.resolve('@react-navigation/native')
|
|
176
|
+
} catch {
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
})()
|
|
180
|
+
|
|
181
|
+
const reactNativeHapticFeedbackTouchablePath = (() => {
|
|
182
|
+
try {
|
|
183
|
+
return path.join(
|
|
184
|
+
path.dirname(
|
|
185
|
+
sootsimPluginRequire.resolve('react-native-haptic-feedback/package.json'),
|
|
186
|
+
),
|
|
187
|
+
SOOTSIM_HAPTIC_FEEDBACK_TOUCHABLE_SOURCE,
|
|
188
|
+
)
|
|
189
|
+
} catch {
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
})()
|
|
193
|
+
|
|
194
|
+
// `@expo/ui/swift-ui/modifiers` is pure JS (every modifier just returns a
|
|
195
|
+
// ModifierConfig via createModifier) and runs from the real bundle unchanged —
|
|
196
|
+
// the runtime stub-registry intentionally does NOT intercept it. but the
|
|
197
|
+
// `@expo/ui/swift-ui` stub alias is a vite PREFIX match, so without an explicit
|
|
198
|
+
// longer alias it captures `.../modifiers` and rewrites it to
|
|
199
|
+
// `<stubs>/expo-ui.ts/modifiers` ("Not a directory"). resolve the real subpath
|
|
200
|
+
// here and alias it first (stubAliases are sorted longest-first, and this is
|
|
201
|
+
// longer than `@expo/ui/swift-ui`, so it wins).
|
|
202
|
+
const expoUiSwiftUiModifiersRealPath = (() => {
|
|
203
|
+
try {
|
|
204
|
+
return sootsimPluginRequire.resolve('@expo/ui/swift-ui/modifiers')
|
|
205
|
+
} catch {
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
})()
|
|
209
|
+
|
|
210
|
+
// the same prefix-alias trap, for the whole Android half of the package.
|
|
211
|
+
// `@expo/ui/jetpack-compose` and its `/modifiers` subpath are deliberately NOT
|
|
212
|
+
// intercepted: every component is a thin `requireNativeView('ExpoUI', …)`
|
|
213
|
+
// wrapper and the Android views come from the compat native seam, so the real
|
|
214
|
+
// upstream JS has to load. Without these two longer aliases the `@expo/ui` stub
|
|
215
|
+
// prefix rewrites them to `<stubs>/expo-ui.ts/jetpack-compose` ("Not a
|
|
216
|
+
// directory") and every Android Expo UI screen fails to build.
|
|
217
|
+
const expoUiJetpackComposeRealAliases = [
|
|
218
|
+
'@expo/ui/jetpack-compose/modifiers',
|
|
219
|
+
'@expo/ui/jetpack-compose',
|
|
220
|
+
].flatMap((specifier) => {
|
|
221
|
+
try {
|
|
222
|
+
return [{ find: specifier, replacement: sootsimPluginRequire.resolve(specifier) }]
|
|
223
|
+
} catch {
|
|
224
|
+
return []
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// `react-native-gesture-handler/ReanimatedSwipeable` and `.../DrawerLayout` are
|
|
229
|
+
// upstream pure JS and should load from the installed package. The root RNGH
|
|
230
|
+
// package still aliases to SootSim's native seam, so add exact subpath aliases
|
|
231
|
+
// before the root stub can rewrite them to
|
|
232
|
+
// `<stubs>/react-native-gesture-handler.ts/<subpath>`.
|
|
233
|
+
function rnghSubpathRealPath(subpath: string): string | null {
|
|
234
|
+
try {
|
|
235
|
+
const packageJsonPath = sootsimPluginRequire.resolve(
|
|
236
|
+
`react-native-gesture-handler/${subpath}/package.json`,
|
|
237
|
+
)
|
|
238
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
239
|
+
const moduleEntry = packageJson?.module
|
|
240
|
+
if (typeof moduleEntry !== 'string' || moduleEntry.length === 0) return null
|
|
241
|
+
const modulePath = path.resolve(path.dirname(packageJsonPath), moduleEntry)
|
|
242
|
+
if (fs.existsSync(modulePath) && fs.statSync(modulePath).isFile()) return modulePath
|
|
243
|
+
const jsPath = `${modulePath}.js`
|
|
244
|
+
if (fs.existsSync(jsPath)) return jsPath
|
|
245
|
+
const indexPath = path.join(modulePath, 'index.js')
|
|
246
|
+
return fs.existsSync(indexPath) ? indexPath : null
|
|
247
|
+
} catch {
|
|
248
|
+
return null
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const rnghReanimatedSwipeableRealPath = rnghSubpathRealPath('ReanimatedSwipeable')
|
|
253
|
+
const rnghDrawerLayoutRealPath = rnghSubpathRealPath('DrawerLayout')
|
|
254
|
+
|
|
255
|
+
const rnghGestureHandlerRootViewContextRealPath = (() => {
|
|
256
|
+
try {
|
|
257
|
+
return path.join(
|
|
258
|
+
path.dirname(
|
|
259
|
+
sootsimPluginRequire.resolve('react-native-gesture-handler/package.json'),
|
|
260
|
+
),
|
|
261
|
+
'lib/module/GestureHandlerRootViewContext.js',
|
|
262
|
+
)
|
|
263
|
+
} catch {
|
|
264
|
+
return null
|
|
265
|
+
}
|
|
266
|
+
})()
|
|
267
|
+
const rnghGestureHandlerRootViewContextShimPath = path.resolve(
|
|
268
|
+
compatStubsDir,
|
|
269
|
+
'rngh-gesture-handler-root-view-context.ts',
|
|
270
|
+
)
|
|
271
|
+
const rnghRootStubPath = path.resolve(compatStubsDir, 'react-native-gesture-handler.ts')
|
|
272
|
+
const rnghLibModuleRoot = rnghGestureHandlerRootViewContextRealPath
|
|
273
|
+
? path.dirname(rnghGestureHandlerRootViewContextRealPath)
|
|
274
|
+
: null
|
|
275
|
+
|
|
276
|
+
const compatWrapperRealPackageAliases: Array<{
|
|
277
|
+
find: string
|
|
278
|
+
replacement: string
|
|
279
|
+
}> = []
|
|
280
|
+
for (const { specifier, packageName } of Object.values(
|
|
281
|
+
SOOTSIM_COMPAT_WRAPPER_REAL_PACKAGES,
|
|
282
|
+
)) {
|
|
283
|
+
let replacement = resolveExportsHiddenSubpath(packageName, (specifier) =>
|
|
284
|
+
sootsimPluginRequire.resolve(specifier),
|
|
285
|
+
)
|
|
286
|
+
if (!replacement) {
|
|
287
|
+
try {
|
|
288
|
+
replacement = sootsimPluginRequire.resolve(packageName)
|
|
289
|
+
} catch {}
|
|
290
|
+
}
|
|
291
|
+
if (!replacement) {
|
|
292
|
+
// a dropped alias only surfaces much later as an opaque
|
|
293
|
+
// "failed to resolve @sootsim-internal/…", so say it here instead.
|
|
294
|
+
console.warn(
|
|
295
|
+
`[sootsim] compat wrapper alias ${specifier} -> ${packageName} did not resolve`,
|
|
296
|
+
)
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
compatWrapperRealPackageAliases.push({ find: specifier, replacement })
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// packages that must always resolve from sootsim to ensure single instances
|
|
303
|
+
// across the renderer/runtime itself. keep this list minimal for external apps.
|
|
304
|
+
const coreOwnedPackages = [
|
|
305
|
+
'react',
|
|
306
|
+
'react-dom',
|
|
307
|
+
'react-reconciler',
|
|
308
|
+
'react/jsx-runtime',
|
|
309
|
+
'react/jsx-dev-runtime',
|
|
310
|
+
'canvaskit-wasm',
|
|
311
|
+
'canvaskit-wasm/full',
|
|
312
|
+
'yoga-layout',
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
const nativePackagePatterns = [
|
|
316
|
+
/^expo-/,
|
|
317
|
+
/^react-native-/,
|
|
318
|
+
/^@react-native\//,
|
|
319
|
+
/^@react-native-community\//,
|
|
320
|
+
/^@expo\//,
|
|
321
|
+
/^expo$/,
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
function getPackageName(source: string): string {
|
|
325
|
+
return source.startsWith('@')
|
|
326
|
+
? source.split('/').slice(0, 2).join('/')
|
|
327
|
+
: source.split('/')[0]
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function isNativePackage(source: string): boolean {
|
|
331
|
+
return nativePackagePatterns.some((p) => p.test(source))
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function moduleExistsIn(pkgName: string, dir: string): boolean {
|
|
335
|
+
try {
|
|
336
|
+
return fs.existsSync(path.join(dir, 'node_modules', pkgName))
|
|
337
|
+
} catch {
|
|
338
|
+
return false
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function sootsim(options: SootSimOptions = {}): Plugin[] {
|
|
343
|
+
const appDir = options.app ? path.resolve(options.app) : ''
|
|
344
|
+
const extraSources = (options.sources || []).map((s) => path.resolve(s))
|
|
345
|
+
const ownedPackages = new Set([...coreOwnedPackages, ...(options.ownedPackages || [])])
|
|
346
|
+
const publishedBrowserPackages = resolvePublishedBrowserPackages(appDir)
|
|
347
|
+
|
|
348
|
+
function isAppSource(filePath: string): boolean {
|
|
349
|
+
// exclude node_modules — only actual app source files
|
|
350
|
+
if (filePath.includes('/node_modules/')) return false
|
|
351
|
+
if (appDir && filePath.startsWith(appDir)) return true
|
|
352
|
+
return extraSources.some((s) => filePath.startsWith(s))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return [
|
|
356
|
+
wsBridgePlugin(),
|
|
357
|
+
reactBridgePlugin(),
|
|
358
|
+
{
|
|
359
|
+
name: 'sootsim-published-browser-internals',
|
|
360
|
+
enforce: 'pre',
|
|
361
|
+
resolveId: {
|
|
362
|
+
filter: { id: /^\./ },
|
|
363
|
+
handler(source, importer) {
|
|
364
|
+
return resolvePublishedBrowserInternal(
|
|
365
|
+
source,
|
|
366
|
+
importer,
|
|
367
|
+
publishedBrowserPackages,
|
|
368
|
+
)
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
sootsimConfigPlugin(appDir, extraSources, publishedBrowserPackages),
|
|
373
|
+
rnghUpstreamInternalResolvePlugin(),
|
|
374
|
+
fixJsxRuntimeExports(),
|
|
375
|
+
flowStripTransform(),
|
|
376
|
+
nodeModulesJsxTransform(),
|
|
377
|
+
// must run before metroNativeResolve — that plugin resolves relative
|
|
378
|
+
// imports like `./bindings` to absolute file paths via the platform-
|
|
379
|
+
// extension lookup, and once it returns a resolved id, downstream
|
|
380
|
+
// resolveId hooks don't see the source string anymore.
|
|
381
|
+
keyboardControllerBindingsRedirect(),
|
|
382
|
+
reanimatedNativeSeamRedirect(),
|
|
383
|
+
manifestNativeSeamRedirect(),
|
|
384
|
+
workletsBabelTransform(isAppSource),
|
|
385
|
+
metroNativeResolve(isAppSource),
|
|
386
|
+
reactNativeRequirePlugin(isAppSource),
|
|
387
|
+
...(appDir ? [externalAppResolvePlugin(appDir, isAppSource, ownedPackages)] : []),
|
|
388
|
+
...(appDir || extraSources.length > 0
|
|
389
|
+
? [externalAppTransformPlugin(isAppSource)]
|
|
390
|
+
: []),
|
|
391
|
+
stubMissingNativeDeps(appDir),
|
|
392
|
+
stubMissingImages(isAppSource),
|
|
393
|
+
]
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function stripViteQuery(id: string): string {
|
|
397
|
+
return id.split('?')[0]
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function isPathInside(child: string, parent: string): boolean {
|
|
401
|
+
const rel = path.relative(parent, child)
|
|
402
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel))
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function rnghUpstreamInternalResolvePlugin(): Plugin {
|
|
406
|
+
return {
|
|
407
|
+
name: 'sootsim-rngh-upstream-internal-resolve',
|
|
408
|
+
enforce: 'pre',
|
|
409
|
+
resolveId: {
|
|
410
|
+
filter: {
|
|
411
|
+
id: /(?:^\.\.\/\.\.(?:\.(?:js|jsx|mjs|ts|tsx))?$|GestureDetector(?:\.(?:js|jsx|mjs|ts|tsx))?$|GestureHandlerRootViewContext(?:\.(?:js|jsx|mjs|ts|tsx))?$|(?:PanGestureHandler|TapGestureHandler|State)(?:\.(?:js|jsx|mjs|ts|tsx))?$)/,
|
|
412
|
+
},
|
|
413
|
+
handler(source, importer) {
|
|
414
|
+
if (!importer || !rnghLibModuleRoot) return null
|
|
415
|
+
const importerPath = stripViteQuery(importer)
|
|
416
|
+
if (!isPathInside(importerPath, rnghLibModuleRoot)) return null
|
|
417
|
+
const sourcePath = source.replace(/\.(js|jsx|mjs|ts|tsx)$/, '')
|
|
418
|
+
if (
|
|
419
|
+
sourcePath === '../..' ||
|
|
420
|
+
sourcePath.endsWith('/handlers/gestures/GestureDetector') ||
|
|
421
|
+
// upstream's DrawerLayout builds on the old-API handlers and the
|
|
422
|
+
// State enum; both come from our seam, not upstream's native modules.
|
|
423
|
+
sourcePath.endsWith('/handlers/PanGestureHandler') ||
|
|
424
|
+
sourcePath.endsWith('/handlers/TapGestureHandler') ||
|
|
425
|
+
sourcePath === '../State'
|
|
426
|
+
) {
|
|
427
|
+
return rnghRootStubPath
|
|
428
|
+
}
|
|
429
|
+
if (sourcePath.endsWith('GestureHandlerRootViewContext')) {
|
|
430
|
+
return rnghGestureHandlerRootViewContextShimPath
|
|
431
|
+
}
|
|
432
|
+
return null
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// shared worker plugin chain for the engine's shell-host build (used by
|
|
439
|
+
// both `vite.config.ts` for dev/watch and `vite.build.config.ts` for prod).
|
|
440
|
+
// keep these here — duplicating the list inline drifts (which is exactly how
|
|
441
|
+
// the prod shell worker shipped without `reanimatedNativeSeamRedirect` and
|
|
442
|
+
// threw "Native part of Reanimated doesn't seem to be initialized" until
|
|
443
|
+
// 2e6a8450).
|
|
444
|
+
//
|
|
445
|
+
// shell-host.ts spawns shell-worker.ts via Vite's declarative
|
|
446
|
+
// `new Worker(new URL(...))` pattern, so shell-worker is a worker chunk of
|
|
447
|
+
// the engine build. apply react-bridge / jsx-runtime fixes plus the
|
|
448
|
+
// reanimated/keyboard-controller native-seam redirects so upstream's
|
|
449
|
+
// reanimated wires up our turbomodule + globals inside the worker too.
|
|
450
|
+
// without these, worker bundles see upstream's original
|
|
451
|
+
// `NativeReanimatedModule.ts`, `TurboModuleRegistry.get('ReanimatedModule')`
|
|
452
|
+
// returns null on web/sootsim, `__reanimatedModuleProxy` never gets set,
|
|
453
|
+
// and the NativeReanimated constructor throws on shell-worker mount.
|
|
454
|
+
//
|
|
455
|
+
// workletsBabelTransform populates `__closure` on user-code worklets so
|
|
456
|
+
// useAnimatedStyle/useDerivedValue/etc. subscribe correctly inside the
|
|
457
|
+
// worker. matching is content-driven today, so the predicate is `() => true`.
|
|
458
|
+
export function engineShellWorkerPlugins(): Plugin[] {
|
|
459
|
+
return [
|
|
460
|
+
workerReactBridgePlugin(),
|
|
461
|
+
fixJsxRuntimeExports(),
|
|
462
|
+
flowStripTransform(),
|
|
463
|
+
nodeModulesJsxTransform(),
|
|
464
|
+
reanimatedNativeSeamRedirect(),
|
|
465
|
+
manifestNativeSeamRedirect(),
|
|
466
|
+
keyboardControllerBindingsRedirect(),
|
|
467
|
+
rnghUpstreamInternalResolvePlugin(),
|
|
468
|
+
workletsBabelTransform(() => true),
|
|
469
|
+
]
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// react-native-keyboard-controller is pure-JS for everything except its
|
|
473
|
+
// native-event seam (`bindings.ts` / `bindings.native.ts`) and RN platform
|
|
474
|
+
// `findNodeHandle` resolution. let upstream's components, hooks, animated
|
|
475
|
+
// module, and KeyboardAvoidingView resolve from node_modules unchanged, and
|
|
476
|
+
// redirect only those native/platform seams.
|
|
477
|
+
//
|
|
478
|
+
// the redirect runs `enforce: 'pre'` so it sees `./bindings` / `../bindings`
|
|
479
|
+
// / `../../bindings` strings before vite's native resolver collapses them to
|
|
480
|
+
// absolute paths. matches by importer path (must be inside upstream's
|
|
481
|
+
// node_modules root) and source basename.
|
|
482
|
+
export function keyboardControllerBindingsRedirect(): Plugin {
|
|
483
|
+
const target = path.resolve(compatStubsDir, 'react-native-keyboard-controller.ts')
|
|
484
|
+
const findNodeHandleTarget = '\0sootsim:rnkc-find-node-handle-native'
|
|
485
|
+
const nativePlatformBasenames = new Set(['findNodeHandle', 'reanimated'])
|
|
486
|
+
const nativeExts = [
|
|
487
|
+
'.ios.tsx',
|
|
488
|
+
'.ios.ts',
|
|
489
|
+
'.ios.jsx',
|
|
490
|
+
'.ios.js',
|
|
491
|
+
'.native.tsx',
|
|
492
|
+
'.native.ts',
|
|
493
|
+
'.native.jsx',
|
|
494
|
+
'.native.js',
|
|
495
|
+
'.native.mjs',
|
|
496
|
+
]
|
|
497
|
+
const resolveNativePlatformFile = (source: string, importer: string): string | null => {
|
|
498
|
+
const base = path.resolve(path.dirname(importer), source)
|
|
499
|
+
for (const ext of nativeExts) {
|
|
500
|
+
const candidate = base + ext
|
|
501
|
+
try {
|
|
502
|
+
if (fs.existsSync(candidate)) return candidate
|
|
503
|
+
} catch {}
|
|
504
|
+
}
|
|
505
|
+
for (const ext of nativeExts) {
|
|
506
|
+
const candidate = path.join(base, 'index' + ext)
|
|
507
|
+
try {
|
|
508
|
+
if (fs.existsSync(candidate)) return candidate
|
|
509
|
+
} catch {}
|
|
510
|
+
}
|
|
511
|
+
return null
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
name: 'sootsim-keyboard-controller-bindings-redirect',
|
|
515
|
+
enforce: 'pre',
|
|
516
|
+
resolveId: {
|
|
517
|
+
filter: {
|
|
518
|
+
id: /(?:^|\/)(?:bindings(?:\.native)?|findNodeHandle|reanimated)$/,
|
|
519
|
+
},
|
|
520
|
+
handler(source, importer) {
|
|
521
|
+
if (!importer) return null
|
|
522
|
+
if (!importer.includes('/react-native-keyboard-controller/')) return null
|
|
523
|
+
// keep bindings imports — strip optional .native and any leading
|
|
524
|
+
// `./`, `../`, `../../` so the depth doesn't matter.
|
|
525
|
+
const basename = source.split('/').pop() ?? ''
|
|
526
|
+
if (nativePlatformBasenames.has(basename)) {
|
|
527
|
+
return (
|
|
528
|
+
resolveNativePlatformFile(source, importer) ??
|
|
529
|
+
(basename === 'findNodeHandle' ? findNodeHandleTarget : null)
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
if (basename !== 'bindings' && basename !== 'bindings.native') return null
|
|
533
|
+
// don't redirect bindings imports that already resolved to our file
|
|
534
|
+
if (importer === target) return null
|
|
535
|
+
return target
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
load: {
|
|
539
|
+
filter: {
|
|
540
|
+
id: new RegExp(`^${virtualIdPrefix}sootsim:rnkc-find-node-handle-native$`),
|
|
541
|
+
},
|
|
542
|
+
handler() {
|
|
543
|
+
return `export { findNodeHandle } from ${JSON.stringify(rnShimPath)};`
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// react-native-reanimated is mostly pure-JS — useSharedValue, useAnimatedStyle,
|
|
550
|
+
// useDerivedValue, withTiming/withSpring/etc., createAnimatedComponent, hooks,
|
|
551
|
+
// layout animations, easing all sit on top of a thin native seam:
|
|
552
|
+
//
|
|
553
|
+
// - src/specs/NativeReanimatedModule (turbomodule that installs __reanimatedModuleProxy)
|
|
554
|
+
// - src/platformFunctions/scrollTo
|
|
555
|
+
// - src/platformFunctions/measure
|
|
556
|
+
// - src/platformFunctions/setNativeProps
|
|
557
|
+
// - src/platformFunctions/dispatchCommand
|
|
558
|
+
// - src/platformFunctions/setGestureState
|
|
559
|
+
//
|
|
560
|
+
// per the project policy of not shimming pure-JS libs, we let upstream resolve
|
|
561
|
+
// from node_modules and redirect ONLY the native seam to our compat stub
|
|
562
|
+
// (one file at packages/compat/src/stubs/react-native-reanimated.ts that
|
|
563
|
+
// exports the right shape — default = turbomodule, named = platform fns).
|
|
564
|
+
//
|
|
565
|
+
// upstream's `assertWorkletsVersion` (DEV-only) imports a build-time script
|
|
566
|
+
// that does `require('react-native-worklets/package.json')`. our flat-file
|
|
567
|
+
// stub for react-native-worklets has no such subpath, so this throws under
|
|
568
|
+
// vite's worker build. redirect the validate script to a no-op stub.
|
|
569
|
+
const reanimatedNativeSeamFiles = new Set([
|
|
570
|
+
'NativeReanimatedModule',
|
|
571
|
+
'scrollTo',
|
|
572
|
+
'measure',
|
|
573
|
+
'setNativeProps',
|
|
574
|
+
'dispatchCommand',
|
|
575
|
+
'setGestureState',
|
|
576
|
+
])
|
|
577
|
+
const reanimatedFabricUtilsRedirectPath = path.resolve(
|
|
578
|
+
compatStubsDir,
|
|
579
|
+
'react-native-reanimated-fabric-utils.ts',
|
|
580
|
+
)
|
|
581
|
+
const reanimatedValidateWorkletsVersionPath = path.resolve(
|
|
582
|
+
compatStubsDir,
|
|
583
|
+
'react-native-reanimated-validate-worklets-version.ts',
|
|
584
|
+
)
|
|
585
|
+
export function reanimatedNativeSeamRedirect(): Plugin {
|
|
586
|
+
const target = path.resolve(compatStubsDir, 'react-native-reanimated.ts')
|
|
587
|
+
return {
|
|
588
|
+
name: 'sootsim-reanimated-native-seam-redirect',
|
|
589
|
+
enforce: 'pre',
|
|
590
|
+
resolveId: {
|
|
591
|
+
filter: {
|
|
592
|
+
id: /(?:^react-native-worklets\/package\.json$|^react-native-reanimated\/scripts\/validate-worklets-version(?:\.js)?$|(?:^|\/)(?:fabricUtils|NativeReanimatedModule|scrollTo|measure|setNativeProps|dispatchCommand|setGestureState)(?:\.(?:native|web|ios|android))?$)/,
|
|
593
|
+
},
|
|
594
|
+
handler(source, importer) {
|
|
595
|
+
// upstream's `assertWorkletsVersion` (DEV-only) imports a build-time
|
|
596
|
+
// script that does `require('react-native-worklets/package.json')`.
|
|
597
|
+
// our flat-file stub for react-native-worklets has no such subpath,
|
|
598
|
+
// so this throws under vite's worker build. resolve the package.json
|
|
599
|
+
// import to a synthetic virtual id and load synthetic JSON below.
|
|
600
|
+
if (source === 'react-native-worklets/package.json') {
|
|
601
|
+
return '\0sootsim:react-native-worklets-package-json'
|
|
602
|
+
}
|
|
603
|
+
if (!importer) return null
|
|
604
|
+
// no-op the assertWorkletsVersion validator — same reason. matches both
|
|
605
|
+
// the bare import and the .js-suffixed resolved form some bundlers see.
|
|
606
|
+
if (
|
|
607
|
+
source === 'react-native-reanimated/scripts/validate-worklets-version' ||
|
|
608
|
+
source === 'react-native-reanimated/scripts/validate-worklets-version.js'
|
|
609
|
+
) {
|
|
610
|
+
return reanimatedValidateWorkletsVersionPath
|
|
611
|
+
}
|
|
612
|
+
if (!importer.includes('/react-native-reanimated/')) return null
|
|
613
|
+
if (importer === target) return null
|
|
614
|
+
// strip optional `.web` / `.native` extension and leading `./`s — the
|
|
615
|
+
// redirect should match regardless of relative depth or platform suffix.
|
|
616
|
+
const basename = (source.split('/').pop() ?? '').replace(
|
|
617
|
+
/\.(native|web|ios|android)$/,
|
|
618
|
+
'',
|
|
619
|
+
)
|
|
620
|
+
// keep fabricUtils on the SootSim Fabric public-instance contract. the
|
|
621
|
+
// redirected module reads the constructor-attached handle for native
|
|
622
|
+
// refs and uses ReactFabric's renderer lookup for composite refs.
|
|
623
|
+
if (
|
|
624
|
+
basename === 'fabricUtils' &&
|
|
625
|
+
importer !== reanimatedFabricUtilsRedirectPath
|
|
626
|
+
) {
|
|
627
|
+
return reanimatedFabricUtilsRedirectPath
|
|
628
|
+
}
|
|
629
|
+
if (!reanimatedNativeSeamFiles.has(basename)) return null
|
|
630
|
+
if (process.env.SOOTSIM_REANIMATED_REDIRECT_DEBUG) {
|
|
631
|
+
console.log(
|
|
632
|
+
'[reanimatedRedirect]',
|
|
633
|
+
basename,
|
|
634
|
+
'<-',
|
|
635
|
+
source,
|
|
636
|
+
'from',
|
|
637
|
+
importer.slice(-80),
|
|
638
|
+
)
|
|
639
|
+
}
|
|
640
|
+
return target
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
load: {
|
|
644
|
+
filter: {
|
|
645
|
+
id: new RegExp(`^${virtualIdPrefix}sootsim:react-native-worklets-package-json$`),
|
|
646
|
+
},
|
|
647
|
+
handler() {
|
|
648
|
+
// synthetic package.json for react-native-worklets — see resolveId
|
|
649
|
+
// above. just enough for upstream's version-validator to read `version`.
|
|
650
|
+
return `export default ${JSON.stringify({ version: '0.0.0-sootsim', name: 'react-native-worklets' })}`
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// generalised native-seam redirect driven by NATIVE_SEAM_MANIFEST. each entry
|
|
657
|
+
// names an upstream package + a list of basenames to redirect to a single
|
|
658
|
+
// stub. agents migrating from wholesale-rewrite stubs to the native-seam
|
|
659
|
+
// pattern append entries here without touching the plugin code.
|
|
660
|
+
//
|
|
661
|
+
// the reanimated plugin above stays separate because it has package-specific
|
|
662
|
+
// edge cases (fabricUtils, validate-worklets-version). new migrations should
|
|
663
|
+
// use the manifest path unless they need similar special handling.
|
|
664
|
+
import { NATIVE_SEAM_MANIFEST } from './native-seam-manifest.ts'
|
|
665
|
+
export function manifestNativeSeamRedirect(): Plugin {
|
|
666
|
+
// build a fast lookup: pkg → { basenames, target }
|
|
667
|
+
const byPkg = new Map<
|
|
668
|
+
string,
|
|
669
|
+
{ basenames: Set<string>; target: string; notes?: string }
|
|
670
|
+
>()
|
|
671
|
+
for (const entry of NATIVE_SEAM_MANIFEST) {
|
|
672
|
+
const existing = byPkg.get(entry.pkg)
|
|
673
|
+
if (existing) {
|
|
674
|
+
for (const b of entry.seamBasenames) existing.basenames.add(b)
|
|
675
|
+
} else {
|
|
676
|
+
byPkg.set(entry.pkg, {
|
|
677
|
+
basenames: new Set(entry.seamBasenames),
|
|
678
|
+
target: entry.target,
|
|
679
|
+
notes: entry.notes,
|
|
680
|
+
})
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const sourceFilter = new RegExp(
|
|
684
|
+
`(?:^|/)(?:${[...byPkg.values()]
|
|
685
|
+
.flatMap(({ basenames }) => [...basenames])
|
|
686
|
+
.map((basename) => basename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
687
|
+
.join('|')})(?:\\.(?:native|web|ios|android))?$`,
|
|
688
|
+
)
|
|
689
|
+
return {
|
|
690
|
+
name: 'sootsim-manifest-native-seam-redirect',
|
|
691
|
+
enforce: 'pre',
|
|
692
|
+
resolveId: {
|
|
693
|
+
filter: { id: sourceFilter },
|
|
694
|
+
handler(source, importer) {
|
|
695
|
+
if (!importer || byPkg.size === 0) return null
|
|
696
|
+
// find the package whose path appears in the importer. matches both
|
|
697
|
+
// `/<pkg>/` (unscoped) and `/@scope/<pkg>/` (scoped — the leading `@`
|
|
698
|
+
// is part of the pkg name in the manifest, e.g. `@notifee/react-native`).
|
|
699
|
+
let match: { basenames: Set<string>; target: string; notes?: string } | undefined
|
|
700
|
+
let matchedPkg: string | undefined
|
|
701
|
+
for (const [pkg, entry] of byPkg) {
|
|
702
|
+
if (importer.includes(`/${pkg}/`)) {
|
|
703
|
+
match = entry
|
|
704
|
+
matchedPkg = pkg
|
|
705
|
+
break
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (!match) return null
|
|
709
|
+
if (importer === match.target) return null
|
|
710
|
+
const basename = (source.split('/').pop() ?? '').replace(
|
|
711
|
+
/\.(native|web|ios|android)$/,
|
|
712
|
+
'',
|
|
713
|
+
)
|
|
714
|
+
if (!match.basenames.has(basename)) return null
|
|
715
|
+
if (process.env.SOOTSIM_NATIVE_SEAM_DEBUG) {
|
|
716
|
+
console.log(
|
|
717
|
+
'[nativeSeamRedirect]',
|
|
718
|
+
`${matchedPkg}:${basename}`,
|
|
719
|
+
'<-',
|
|
720
|
+
source,
|
|
721
|
+
'from',
|
|
722
|
+
importer.slice(-80),
|
|
723
|
+
match.notes ? `(${match.notes})` : '',
|
|
724
|
+
)
|
|
725
|
+
}
|
|
726
|
+
return match.target
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// start WS bridge server for debug CLI connectivity. exported so shell
|
|
733
|
+
// vite can install it standalone (engine builds via `vite build --watch`,
|
|
734
|
+
// which doesn't fire configureServer — the bridge has to run on shell).
|
|
735
|
+
export function wsBridgePlugin(): Plugin {
|
|
736
|
+
let bridgeHost: {
|
|
737
|
+
startAsync?: (options?: { silent?: boolean }) => Promise<number>
|
|
738
|
+
close?: () => Promise<void> | void
|
|
739
|
+
} | null = null
|
|
740
|
+
// the port the bridge ACTUALLY bound, which is not necessarily the one it
|
|
741
|
+
// asked for: the host scans forward from its preferred port on EADDRINUSE.
|
|
742
|
+
// the page has to be told this value. when it derived its own guess from
|
|
743
|
+
// the shell's http port instead, any box where 7668 was already taken sent
|
|
744
|
+
// the page dialing a port nobody listens on, and the only symptom was a
|
|
745
|
+
// websocket console error during boot.
|
|
746
|
+
let boundPort: Promise<number | null> | null = null
|
|
747
|
+
return {
|
|
748
|
+
name: 'sootsim-ws-bridge',
|
|
749
|
+
apply: 'serve',
|
|
750
|
+
configureServer(server) {
|
|
751
|
+
if (boundPort) return
|
|
752
|
+
// lazy import to avoid pulling ws into the main bundle.
|
|
753
|
+
// note: the .ts extension is required on vite 8 / node ESM — without
|
|
754
|
+
// it, import() throws "Cannot find module" and the bridge silently
|
|
755
|
+
// never starts.
|
|
756
|
+
boundPort = import('./host/bridge-host.ts')
|
|
757
|
+
.then(({ SootSimBridgeHost }) => {
|
|
758
|
+
const host = new SootSimBridgeHost({
|
|
759
|
+
port: resolveSootsimBridgePort({
|
|
760
|
+
explicitPort: process.env.SOOTSIM_BRIDGE_PORT,
|
|
761
|
+
portOffset: process.env.PORT_OFFSET,
|
|
762
|
+
}),
|
|
763
|
+
writeDevLockfile: true,
|
|
764
|
+
// resolved on every lockfile heartbeat: the address is null until
|
|
765
|
+
// vite's http server is listening, then the real bound port. lets
|
|
766
|
+
// CLI commands resolve the shell base URL from the same lockfile
|
|
767
|
+
// that names the bridge port (one world, no split-brain).
|
|
768
|
+
getShellPort: () => {
|
|
769
|
+
const addr = server.httpServer?.address()
|
|
770
|
+
if (addr && typeof addr === 'object') return addr.port
|
|
771
|
+
return server.config.server.port || null
|
|
772
|
+
},
|
|
773
|
+
})
|
|
774
|
+
bridgeHost = host
|
|
775
|
+
// startAsync, not start: the whole point is to learn the bound port.
|
|
776
|
+
return host.startAsync({ silent: true })
|
|
777
|
+
})
|
|
778
|
+
.catch((err: unknown) => {
|
|
779
|
+
// log but don't crash — some environments (e.g. production builds)
|
|
780
|
+
// don't have ws available. silently swallowing has caused debugging
|
|
781
|
+
// pain; print the message instead.
|
|
782
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
783
|
+
console.warn('[rnx] ws bridge failed to start:', message)
|
|
784
|
+
return null
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
// cleanup on server close (HMR restart)
|
|
788
|
+
server.httpServer?.on('close', () => {
|
|
789
|
+
if (bridgeHost) {
|
|
790
|
+
void bridgeHost.close?.()
|
|
791
|
+
bridgeHost = null
|
|
792
|
+
}
|
|
793
|
+
boundPort = null
|
|
794
|
+
})
|
|
795
|
+
},
|
|
796
|
+
// publish the bound port to the page. head-prepend so it lands before the
|
|
797
|
+
// module script that reads it. `vite build` never runs configureServer, so
|
|
798
|
+
// boundPort stays null there and nothing is injected.
|
|
799
|
+
//
|
|
800
|
+
// `||` rather than a plain assignment, and do not "simplify" it away: the
|
|
801
|
+
// playwright driver and the electron preload inject the port of the daemon
|
|
802
|
+
// they actually own via addInitScript, which runs before any page script.
|
|
803
|
+
// that value is authoritative and must win, so this dev server only ever
|
|
804
|
+
// fills in a blank — it never overwrites a port someone else published.
|
|
805
|
+
async transformIndexHtml() {
|
|
806
|
+
const port = boundPort ? await boundPort : null
|
|
807
|
+
if (port == null) return
|
|
808
|
+
return [
|
|
809
|
+
{
|
|
810
|
+
tag: 'script',
|
|
811
|
+
injectTo: 'head-prepend' as const,
|
|
812
|
+
children: `window.__sootsimBridgePort=window.__sootsimBridgePort||${port};`,
|
|
813
|
+
},
|
|
814
|
+
]
|
|
815
|
+
},
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// applies resolve, define, optimizeDeps config
|
|
820
|
+
function sootsimConfigPlugin(
|
|
821
|
+
appDir: string,
|
|
822
|
+
extraSources: string[],
|
|
823
|
+
publishedBrowserPackages: PublishedBrowserPackage[],
|
|
824
|
+
): Plugin {
|
|
825
|
+
return {
|
|
826
|
+
name: 'sootsim-config',
|
|
827
|
+
config(_, { mode }) {
|
|
828
|
+
const fsAllow = ['.']
|
|
829
|
+
for (const src of extraSources) fsAllow.push(src)
|
|
830
|
+
if (appDir) {
|
|
831
|
+
fsAllow.push(appDir)
|
|
832
|
+
const appNodeModules = path.join(appDir, 'node_modules')
|
|
833
|
+
if (fs.existsSync(appNodeModules)) fsAllow.push(appNodeModules)
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// build alias list from builtin stubs
|
|
837
|
+
const stubAliases = Object.entries(builtinStubs)
|
|
838
|
+
// sort longest keys first so subpath aliases (e.g. foo/Bar) match before
|
|
839
|
+
// their parent package (e.g. foo) — vite processes aliases in order
|
|
840
|
+
.sort((a, b) => b[0].length - a[0].length)
|
|
841
|
+
.map(([find, file]) => ({
|
|
842
|
+
find,
|
|
843
|
+
replacement: path.resolve(compatStubsDir, file),
|
|
844
|
+
}))
|
|
845
|
+
|
|
846
|
+
const publishedBrowserEntryAliases = publishedBrowserPackages.map(
|
|
847
|
+
({ specifier, browserEntry }) => ({
|
|
848
|
+
find: new RegExp(`^${specifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`),
|
|
849
|
+
replacement: browserEntry,
|
|
850
|
+
}),
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
const dedupe = ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime']
|
|
854
|
+
|
|
855
|
+
// internal sootsim builds must keep the entire Tamagui package family on a
|
|
856
|
+
// single module graph. mixing raw @fs modules with prebundled .vite/deps
|
|
857
|
+
// versions breaks context identity (Adapt/Portal/ZIndex/etc).
|
|
858
|
+
if (!appDir) {
|
|
859
|
+
dedupe.push(...internalTamaguiPackages)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const optimizeDepsExclude = [
|
|
863
|
+
'@tamagui/config',
|
|
864
|
+
'@tamagui/demos',
|
|
865
|
+
'react-native',
|
|
866
|
+
// tamagui native variants reference RN internals that the metro transform
|
|
867
|
+
// handles — must go through sootsim plugin, not rolldown pre-bundling
|
|
868
|
+
...(!appDir ? internalTamaguiPackages : []),
|
|
869
|
+
// ships .js with JSX that rolldown can't parse
|
|
870
|
+
'react-native-actions-sheet',
|
|
871
|
+
// these packages import `react-native-reanimated` and `react-native-gesture-handler`,
|
|
872
|
+
// both of which we alias to local stubs. pre-bundling would resolve those imports
|
|
873
|
+
// against the REAL packages (the alias only applies to main-plugin resolveId), so
|
|
874
|
+
// the resulting dep has a different reanimated instance than the app graph. keep
|
|
875
|
+
// them served live so our plugin aliases kick in.
|
|
876
|
+
'@gorhom/bottom-sheet',
|
|
877
|
+
'@gorhom/portal',
|
|
878
|
+
// these reviewed packages publish a supported browser artifact. keep
|
|
879
|
+
// them on the live graph so the exact alias below wins over their
|
|
880
|
+
// `react-native` export during dependency optimization too.
|
|
881
|
+
...Object.values(SOOTSIM_COMPAT_PUBLISHED_BROWSER_ENTRIES).map(
|
|
882
|
+
({ specifier }) => specifier,
|
|
883
|
+
),
|
|
884
|
+
]
|
|
885
|
+
|
|
886
|
+
return {
|
|
887
|
+
define: {
|
|
888
|
+
'process.env.NODE_ENV': JSON.stringify(mode),
|
|
889
|
+
'process.env.TEST_NATIVE_PLATFORM': JSON.stringify(''),
|
|
890
|
+
'process.env.TAMAGUI_TARGET': JSON.stringify('native'),
|
|
891
|
+
// expo sdk 57's winter runtime (expo/src/winter/runtime.native.ts)
|
|
892
|
+
// replaces globalThis.fetch with a native implementation whose
|
|
893
|
+
// Response class extends ExpoFetchModule.NativeResponse. that native
|
|
894
|
+
// module does not exist in a browser, so constructing any response
|
|
895
|
+
// throws `this.addListener is not a function` and every request the
|
|
896
|
+
// app makes fails. upstream's own web build keeps the platform fetch
|
|
897
|
+
// (src/winter/fetch/fetch.web.ts is `globalThis.fetch`), and this is
|
|
898
|
+
// upstream's supported switch for that — it removes the native fetch
|
|
899
|
+
// install, leaving the browser's streaming fetch in place.
|
|
900
|
+
'process.env.EXPO_PUBLIC_USE_RN_FETCH': JSON.stringify('1'),
|
|
901
|
+
global: 'globalThis',
|
|
902
|
+
},
|
|
903
|
+
optimizeDeps: {
|
|
904
|
+
include: [
|
|
905
|
+
'react',
|
|
906
|
+
'react-dom',
|
|
907
|
+
'react-dom/client',
|
|
908
|
+
'react-reconciler',
|
|
909
|
+
'react-reconciler/constants',
|
|
910
|
+
'react/jsx-runtime',
|
|
911
|
+
'react/jsx-dev-runtime',
|
|
912
|
+
'@react-native/normalize-color',
|
|
913
|
+
// CJS deps transitively imported by @gorhom/bottom-sheet and
|
|
914
|
+
// @gorhom/portal (both excluded above). vite's CJS→ESM interop
|
|
915
|
+
// only kicks in via pre-bundle, so these must be pre-bundled even
|
|
916
|
+
// though their importers aren't.
|
|
917
|
+
'invariant',
|
|
918
|
+
'nanoid/non-secure',
|
|
919
|
+
],
|
|
920
|
+
exclude: optimizeDepsExclude,
|
|
921
|
+
// vite 8: rolldown replaces esbuild for dep optimization
|
|
922
|
+
rolldownOptions: {
|
|
923
|
+
resolve: {
|
|
924
|
+
conditionNames: ['react-native', 'import'],
|
|
925
|
+
mainFields: ['react-native', 'module', 'jsnext:main', 'jsnext'],
|
|
926
|
+
extensions: [
|
|
927
|
+
'.ios.tsx',
|
|
928
|
+
'.ios.ts',
|
|
929
|
+
'.ios.jsx',
|
|
930
|
+
'.ios.js',
|
|
931
|
+
'.native.tsx',
|
|
932
|
+
'.native.ts',
|
|
933
|
+
'.native.jsx',
|
|
934
|
+
'.native.js',
|
|
935
|
+
'.native.mjs',
|
|
936
|
+
'.mjs',
|
|
937
|
+
'.js',
|
|
938
|
+
'.mts',
|
|
939
|
+
'.ts',
|
|
940
|
+
'.jsx',
|
|
941
|
+
'.tsx',
|
|
942
|
+
'.json',
|
|
943
|
+
],
|
|
944
|
+
},
|
|
945
|
+
plugins: [
|
|
946
|
+
{
|
|
947
|
+
name: 'sootsim-published-browser-internals',
|
|
948
|
+
resolveId(source: string, importer: string | undefined) {
|
|
949
|
+
const replacement = resolvePublishedBrowserInternal(
|
|
950
|
+
source,
|
|
951
|
+
importer,
|
|
952
|
+
publishedBrowserPackages,
|
|
953
|
+
)
|
|
954
|
+
return replacement ? { id: replacement } : null
|
|
955
|
+
},
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
name: 'sootsim-stub-images',
|
|
959
|
+
resolveId(source: string, importer: string | undefined) {
|
|
960
|
+
if (
|
|
961
|
+
/\.(jpg|png|gif)$/.test(source) &&
|
|
962
|
+
importer?.includes('node_modules')
|
|
963
|
+
) {
|
|
964
|
+
return { id: '\0stub-image' }
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
load(id: string) {
|
|
968
|
+
if (id === '\0stub-image') return 'export default ""'
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
],
|
|
972
|
+
},
|
|
973
|
+
},
|
|
974
|
+
build: { target: 'esnext' },
|
|
975
|
+
oxc: { target: 'esnext' },
|
|
976
|
+
server: {
|
|
977
|
+
fs: { allow: fsAllow },
|
|
978
|
+
},
|
|
979
|
+
resolve: {
|
|
980
|
+
dedupe,
|
|
981
|
+
conditions: ['react-native'],
|
|
982
|
+
mainFields: ['react-native', 'module', 'jsnext:main', 'jsnext'],
|
|
983
|
+
extensions: [
|
|
984
|
+
'.ios.tsx',
|
|
985
|
+
'.ios.ts',
|
|
986
|
+
'.ios.jsx',
|
|
987
|
+
'.ios.js',
|
|
988
|
+
'.native.tsx',
|
|
989
|
+
'.native.ts',
|
|
990
|
+
'.native.jsx',
|
|
991
|
+
'.native.js',
|
|
992
|
+
'.native.mjs',
|
|
993
|
+
'.mjs',
|
|
994
|
+
'.js',
|
|
995
|
+
'.mts',
|
|
996
|
+
'.ts',
|
|
997
|
+
'.jsx',
|
|
998
|
+
'.tsx',
|
|
999
|
+
'.json',
|
|
1000
|
+
],
|
|
1001
|
+
alias: [
|
|
1002
|
+
...sootsimBrowserSourceAliases,
|
|
1003
|
+
...publishedBrowserEntryAliases,
|
|
1004
|
+
...Object.entries(rnLibraryStubs).map(([find, replacement]) => ({
|
|
1005
|
+
find,
|
|
1006
|
+
replacement,
|
|
1007
|
+
})),
|
|
1008
|
+
{ find: /^react-native\/Libraries\/.*/, replacement: rnDeepStubDefault },
|
|
1009
|
+
{ find: 'react-native', replacement: rnShimPath },
|
|
1010
|
+
{ find: 'react-native-web', replacement: rnShimPath },
|
|
1011
|
+
// resolved-path alias for the synthetic specifier the
|
|
1012
|
+
// @react-navigation/native compat wrapper imports from. terminates
|
|
1013
|
+
// the wrapper's own real-package import without re-entering the
|
|
1014
|
+
// alias above. omit the entry if the package isn't installed —
|
|
1015
|
+
// the wrapper file simply won't be loaded in that case.
|
|
1016
|
+
...(reactNavigationNativeRealPath
|
|
1017
|
+
? [
|
|
1018
|
+
{
|
|
1019
|
+
find: '@sootsim-internal/react-navigation-native-real',
|
|
1020
|
+
replacement: reactNavigationNativeRealPath,
|
|
1021
|
+
},
|
|
1022
|
+
]
|
|
1023
|
+
: []),
|
|
1024
|
+
...(reactNativeHapticFeedbackTouchablePath
|
|
1025
|
+
? [
|
|
1026
|
+
{
|
|
1027
|
+
find: SOOTSIM_HAPTIC_FEEDBACK_TOUCHABLE_SPECIFIER,
|
|
1028
|
+
replacement: reactNativeHapticFeedbackTouchablePath,
|
|
1029
|
+
},
|
|
1030
|
+
]
|
|
1031
|
+
: []),
|
|
1032
|
+
...compatWrapperRealPackageAliases,
|
|
1033
|
+
...(expoUiSwiftUiModifiersRealPath
|
|
1034
|
+
? [
|
|
1035
|
+
{
|
|
1036
|
+
find: '@expo/ui/swift-ui/modifiers',
|
|
1037
|
+
replacement: expoUiSwiftUiModifiersRealPath,
|
|
1038
|
+
},
|
|
1039
|
+
]
|
|
1040
|
+
: []),
|
|
1041
|
+
...expoUiJetpackComposeRealAliases,
|
|
1042
|
+
...(rnghReanimatedSwipeableRealPath
|
|
1043
|
+
? [
|
|
1044
|
+
{
|
|
1045
|
+
find: /^react-native-gesture-handler\/ReanimatedSwipeable$/,
|
|
1046
|
+
replacement: rnghReanimatedSwipeableRealPath,
|
|
1047
|
+
},
|
|
1048
|
+
]
|
|
1049
|
+
: []),
|
|
1050
|
+
...(rnghDrawerLayoutRealPath
|
|
1051
|
+
? [
|
|
1052
|
+
{
|
|
1053
|
+
find: /^react-native-gesture-handler\/DrawerLayout$/,
|
|
1054
|
+
replacement: rnghDrawerLayoutRealPath,
|
|
1055
|
+
},
|
|
1056
|
+
]
|
|
1057
|
+
: []),
|
|
1058
|
+
...stubAliases,
|
|
1059
|
+
],
|
|
1060
|
+
},
|
|
1061
|
+
}
|
|
1062
|
+
},
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// redirect `import ... from 'react'` to react-bridge for sootsim, compat, and
|
|
1067
|
+
// react-reconciler code. this enables runtime React version switching for
|
|
1068
|
+
// external app bundles — bindReact(appReact) swaps which React all code uses.
|
|
1069
|
+
function reactBridgePlugin(): Plugin {
|
|
1070
|
+
let redirectCount = 0
|
|
1071
|
+
return {
|
|
1072
|
+
name: 'sootsim-react-bridge',
|
|
1073
|
+
apply: 'serve',
|
|
1074
|
+
enforce: 'pre',
|
|
1075
|
+
resolveId: {
|
|
1076
|
+
filter: { id: /^react$/ },
|
|
1077
|
+
async handler(_source, importer, options) {
|
|
1078
|
+
if (!importer) return null
|
|
1079
|
+
|
|
1080
|
+
// skip during dep scanning / SSR — only active in browser serve mode
|
|
1081
|
+
if ((options as { scan?: boolean })?.scan || options?.ssr) return null
|
|
1082
|
+
|
|
1083
|
+
// only redirect imports from sootsim, sootsim-engine, compat, and react-reconciler
|
|
1084
|
+
const shouldRedirect =
|
|
1085
|
+
importer.includes('/sootsim/src/') ||
|
|
1086
|
+
importer.includes('/sootsim-engine/src/') ||
|
|
1087
|
+
importer.includes('/compat/src/') ||
|
|
1088
|
+
importer.includes('react-reconciler')
|
|
1089
|
+
|
|
1090
|
+
if (!shouldRedirect) return null
|
|
1091
|
+
|
|
1092
|
+
// don't redirect react-bridge's own import of react (avoid infinite loop)
|
|
1093
|
+
if (importer.includes('react-bridge')) return null
|
|
1094
|
+
|
|
1095
|
+
// stub-registry needs the real React (with __CLIENT_INTERNALS) for bundle stubs
|
|
1096
|
+
if (importer.includes('stub-registry')) return null
|
|
1097
|
+
|
|
1098
|
+
redirectCount++
|
|
1099
|
+
if (redirectCount <= 10) {
|
|
1100
|
+
console.log(
|
|
1101
|
+
'[react-bridge-plugin] REDIRECTING:',
|
|
1102
|
+
importer.split('/').slice(-3).join('/'),
|
|
1103
|
+
)
|
|
1104
|
+
} else if (redirectCount === 11) {
|
|
1105
|
+
console.log('[react-bridge-plugin] ... more redirects')
|
|
1106
|
+
}
|
|
1107
|
+
return { id: reactBridgePath, external: false }
|
|
1108
|
+
},
|
|
1109
|
+
},
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// worker-compatible react-bridge redirect. unlike the main plugin, this one
|
|
1114
|
+
// also applies during builds since workers are built, not served. exported for
|
|
1115
|
+
// use in worker.plugins configuration.
|
|
1116
|
+
export function workerReactBridgePlugin(): Plugin {
|
|
1117
|
+
return {
|
|
1118
|
+
name: 'sootsim-worker-react-bridge',
|
|
1119
|
+
enforce: 'pre',
|
|
1120
|
+
resolveId: {
|
|
1121
|
+
filter: { id: /^react$/ },
|
|
1122
|
+
async handler(_source, importer, options) {
|
|
1123
|
+
if (!importer) return null
|
|
1124
|
+
|
|
1125
|
+
// skip during dep scanning / SSR
|
|
1126
|
+
if ((options as { scan?: boolean })?.scan || options?.ssr) return null
|
|
1127
|
+
|
|
1128
|
+
// don't redirect react-bridge's own import of react (avoid infinite loop)
|
|
1129
|
+
if (importer.includes('react-bridge')) return null
|
|
1130
|
+
|
|
1131
|
+
// stub-registry needs the real React (with __CLIENT_INTERNALS) for bundle stubs
|
|
1132
|
+
if (importer.includes('stub-registry')) return null
|
|
1133
|
+
|
|
1134
|
+
// redirect react imports from sootsim, sootsim-engine, compat, react-reconciler,
|
|
1135
|
+
// and any pure-JS RN-ecosystem package the bundle pulls in. without this
|
|
1136
|
+
// redirect, packages like react-native-drawer-layout / use-latest-callback
|
|
1137
|
+
// call into a separate React instance than the renderer/bridge does, so
|
|
1138
|
+
// their useRef/useEffect refs collide with the engine's hooks state and
|
|
1139
|
+
// we hit "Maximum update depth exceeded" infinite-loop renders.
|
|
1140
|
+
const shouldRedirect =
|
|
1141
|
+
importer.includes('/sootsim/src/') ||
|
|
1142
|
+
importer.includes('/sootsim-engine/src/') ||
|
|
1143
|
+
importer.includes('/compat/src/') ||
|
|
1144
|
+
importer.includes('react-reconciler') ||
|
|
1145
|
+
importer.includes('/node_modules/')
|
|
1146
|
+
|
|
1147
|
+
if (!shouldRedirect) return null
|
|
1148
|
+
|
|
1149
|
+
return { id: reactBridgePath, external: false }
|
|
1150
|
+
},
|
|
1151
|
+
},
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// react 19's jsx-runtime.development.js wraps exports.jsx inside a conditional
|
|
1156
|
+
// IIFE, so esbuild's static CJS analysis can't detect the named exports during
|
|
1157
|
+
// pre-bundling. this plugin wraps the real pre-bundled module with explicit exports.
|
|
1158
|
+
// exported for use in worker.plugins configuration.
|
|
1159
|
+
export function fixJsxRuntimeExports(): Plugin {
|
|
1160
|
+
return {
|
|
1161
|
+
name: 'sootsim-fix-jsx-runtime',
|
|
1162
|
+
enforce: 'pre',
|
|
1163
|
+
resolveId: {
|
|
1164
|
+
filter: { id: /^react\/jsx-(?:dev-)?runtime$/ },
|
|
1165
|
+
async handler(source, importer, options) {
|
|
1166
|
+
const resolved = await this.resolve(source, importer, {
|
|
1167
|
+
...options,
|
|
1168
|
+
skipSelf: true,
|
|
1169
|
+
})
|
|
1170
|
+
if (!resolved) return null
|
|
1171
|
+
const prefix =
|
|
1172
|
+
source === 'react/jsx-runtime'
|
|
1173
|
+
? '\0sootsim:jsx-runtime:'
|
|
1174
|
+
: '\0sootsim:jsx-dev-runtime:'
|
|
1175
|
+
return prefix + resolved.id
|
|
1176
|
+
},
|
|
1177
|
+
},
|
|
1178
|
+
load: {
|
|
1179
|
+
filter: {
|
|
1180
|
+
id: new RegExp(`^${virtualIdPrefix}sootsim:jsx-(?:dev-)?runtime:`),
|
|
1181
|
+
},
|
|
1182
|
+
handler(id) {
|
|
1183
|
+
if (id.startsWith('\0sootsim:jsx-runtime:')) {
|
|
1184
|
+
const realId = id.slice('\0sootsim:jsx-runtime:'.length)
|
|
1185
|
+
return [
|
|
1186
|
+
`import * as _mod from ${JSON.stringify(realId)};`,
|
|
1187
|
+
`const _m = _mod.default || _mod;`,
|
|
1188
|
+
`export const jsx = _m.jsx || _mod.jsx;`,
|
|
1189
|
+
`export const jsxs = _m.jsxs || _mod.jsxs;`,
|
|
1190
|
+
`export const Fragment = _m.Fragment || _mod.Fragment;`,
|
|
1191
|
+
].join('\n')
|
|
1192
|
+
}
|
|
1193
|
+
if (id.startsWith('\0sootsim:jsx-dev-runtime:')) {
|
|
1194
|
+
const realId = id.slice('\0sootsim:jsx-dev-runtime:'.length)
|
|
1195
|
+
// react/jsx-runtime is already wrapped by this plugin (above) to expose
|
|
1196
|
+
// { jsx, jsxs, Fragment } as named exports, so read them directly from
|
|
1197
|
+
// the namespace — no `_runtime.default || _runtime` fallback needed.
|
|
1198
|
+
// rolldown statically knows the wrapped module has no default export
|
|
1199
|
+
// and would emit IMPORT_IS_UNDEFINED warnings on every build.
|
|
1200
|
+
return [
|
|
1201
|
+
`import * as _mod from ${JSON.stringify(realId)};`,
|
|
1202
|
+
`import * as _runtime from "react/jsx-runtime";`,
|
|
1203
|
+
`const _m = _mod.default || _mod;`,
|
|
1204
|
+
// production React builds may not expose jsxDEV from react/jsx-dev-runtime.
|
|
1205
|
+
// fall back to jsx so transformed modules still execute.
|
|
1206
|
+
`export const jsxDEV = _m.jsxDEV || _mod.jsxDEV || _m.jsx || _mod.jsx || _runtime.jsx;`,
|
|
1207
|
+
`export const Fragment = _m.Fragment || _mod.Fragment || _runtime.Fragment;`,
|
|
1208
|
+
].join('\n')
|
|
1209
|
+
}
|
|
1210
|
+
return null
|
|
1211
|
+
},
|
|
1212
|
+
},
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// esbuild plugin for optimizeDeps pre-bundling
|
|
1217
|
+
function sootsimEsbuildPlugin(appDir: string) {
|
|
1218
|
+
const platformExts = [
|
|
1219
|
+
'.ios.tsx',
|
|
1220
|
+
'.ios.ts',
|
|
1221
|
+
'.ios.jsx',
|
|
1222
|
+
'.ios.js',
|
|
1223
|
+
'.native.tsx',
|
|
1224
|
+
'.native.ts',
|
|
1225
|
+
'.native.jsx',
|
|
1226
|
+
'.native.js',
|
|
1227
|
+
'.native.mjs',
|
|
1228
|
+
]
|
|
1229
|
+
|
|
1230
|
+
function isAppSourceEsbuild(filePath: string): boolean {
|
|
1231
|
+
return !!(appDir && filePath.startsWith(appDir))
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
return {
|
|
1235
|
+
name: 'sootsim-esbuild',
|
|
1236
|
+
setup(build: any) {
|
|
1237
|
+
// metro-style .ios > .native resolution
|
|
1238
|
+
build.onResolve({ filter: /^\./ }, (args: any) => {
|
|
1239
|
+
if (!args.importer.includes('node_modules') && !isAppSourceEsbuild(args.importer))
|
|
1240
|
+
return null
|
|
1241
|
+
const dir = path.dirname(args.importer)
|
|
1242
|
+
const resolved = path.resolve(dir, args.path)
|
|
1243
|
+
for (const ext of platformExts) {
|
|
1244
|
+
const candidate = resolved + ext
|
|
1245
|
+
try {
|
|
1246
|
+
if (fs.existsSync(candidate)) return { path: candidate }
|
|
1247
|
+
} catch {}
|
|
1248
|
+
}
|
|
1249
|
+
for (const ext of platformExts) {
|
|
1250
|
+
const candidate = path.join(resolved, 'index' + ext)
|
|
1251
|
+
try {
|
|
1252
|
+
if (fs.existsSync(candidate)) return { path: candidate }
|
|
1253
|
+
} catch {}
|
|
1254
|
+
}
|
|
1255
|
+
const allExts = [...platformExts, '.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs']
|
|
1256
|
+
for (const ext of allExts) {
|
|
1257
|
+
if (args.path.endsWith(ext)) {
|
|
1258
|
+
const base = resolved.slice(0, -ext.length)
|
|
1259
|
+
for (const pext of platformExts) {
|
|
1260
|
+
const candidate = base + pext
|
|
1261
|
+
try {
|
|
1262
|
+
if (fs.existsSync(candidate)) return { path: candidate }
|
|
1263
|
+
} catch {}
|
|
1264
|
+
}
|
|
1265
|
+
break
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return null
|
|
1269
|
+
})
|
|
1270
|
+
|
|
1271
|
+
// react-native deep path stubs
|
|
1272
|
+
for (const [importPath, stubPath] of Object.entries(rnLibraryStubs)) {
|
|
1273
|
+
build.onResolve(
|
|
1274
|
+
{ filter: new RegExp(`^${importPath.replace(/\//g, '\\/')}$`) },
|
|
1275
|
+
() => ({
|
|
1276
|
+
path: stubPath,
|
|
1277
|
+
}),
|
|
1278
|
+
)
|
|
1279
|
+
}
|
|
1280
|
+
build.onResolve({ filter: /^react-native\/Libraries\// }, (args: any) => {
|
|
1281
|
+
return { path: rnLibraryStubs[args.path] || rnDeepStubDefault }
|
|
1282
|
+
})
|
|
1283
|
+
|
|
1284
|
+
// resolve builtin stubs (react-native-safe-area-context, expo-*, etc.)
|
|
1285
|
+
for (const [pkgName, stubFile] of Object.entries(builtinStubs)) {
|
|
1286
|
+
const stubPath = path.resolve(compatStubsDir, stubFile)
|
|
1287
|
+
const escaped = pkgName.replace(/[.*+?^${}()|[\]\\/@-]/g, '\\$&')
|
|
1288
|
+
build.onResolve({ filter: new RegExp(`^${escaped}$`) }, () => ({
|
|
1289
|
+
path: stubPath,
|
|
1290
|
+
}))
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// external app node_modules resolution — use esbuild's own resolve
|
|
1294
|
+
// so exports conditions are respected (not CJS require.resolve which ignores them)
|
|
1295
|
+
if (appDir) {
|
|
1296
|
+
const owned = new Set(coreOwnedPackages)
|
|
1297
|
+
build.onResolve({ filter: /^[^.]/ }, async (args: any) => {
|
|
1298
|
+
if (args.pluginData?.fromAppResolve) return null
|
|
1299
|
+
if (
|
|
1300
|
+
!isAppSourceEsbuild(args.importer) &&
|
|
1301
|
+
!args.importer.includes(path.join(appDir, 'node_modules'))
|
|
1302
|
+
)
|
|
1303
|
+
return null
|
|
1304
|
+
const src = args.path
|
|
1305
|
+
if (src.startsWith('\0')) return null
|
|
1306
|
+
if (owned.has(src) || owned.has(getPackageName(src))) return null
|
|
1307
|
+
if (src === 'react-native' || src.startsWith('react-native/')) return null
|
|
1308
|
+
try {
|
|
1309
|
+
const result = await build.resolve(src, {
|
|
1310
|
+
resolveDir: appDir,
|
|
1311
|
+
pluginData: { fromAppResolve: true },
|
|
1312
|
+
})
|
|
1313
|
+
if (result.errors.length) return null
|
|
1314
|
+
return { path: result.path }
|
|
1315
|
+
} catch {
|
|
1316
|
+
return null
|
|
1317
|
+
}
|
|
1318
|
+
})
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// auto-stub missing native packages
|
|
1322
|
+
build.onResolve(
|
|
1323
|
+
{
|
|
1324
|
+
filter:
|
|
1325
|
+
/^(expo-|react-native-|@react-native\/|@react-native-community\/|@expo\/|expo$)/,
|
|
1326
|
+
},
|
|
1327
|
+
(args: any) => {
|
|
1328
|
+
const pkgName = getPackageName(args.path)
|
|
1329
|
+
if (moduleExistsIn(pkgName, sootsimRoot)) return null
|
|
1330
|
+
if (appDir && moduleExistsIn(pkgName, appDir)) return null
|
|
1331
|
+
return { path: nativeAutoStubPath }
|
|
1332
|
+
},
|
|
1333
|
+
)
|
|
1334
|
+
|
|
1335
|
+
// stub missing image imports
|
|
1336
|
+
build.onResolve({ filter: /\.(jpg|png|gif)$/ }, (args: any) => {
|
|
1337
|
+
if (args.importer.includes('node_modules')) {
|
|
1338
|
+
return { path: args.path, namespace: 'stub-image' }
|
|
1339
|
+
}
|
|
1340
|
+
return null
|
|
1341
|
+
})
|
|
1342
|
+
build.onLoad({ filter: /.*/, namespace: 'stub-image' }, () => ({
|
|
1343
|
+
contents: 'export default ""',
|
|
1344
|
+
loader: 'js',
|
|
1345
|
+
}))
|
|
1346
|
+
},
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// handle .js files containing JSX from node_modules during production builds.
|
|
1351
|
+
// upstream `react-native-reanimated` and downstream consumers (
|
|
1352
|
+
// `react-native-keyboard-controller`, `react-native-gesture-handler`,
|
|
1353
|
+
// `react-native-screens`, sootsim's own fixtures + RN compat source) ship
|
|
1354
|
+
// raw source containing `'worklet'` directives and inline updaters to
|
|
1355
|
+
// auto-workletizable hooks. the `react-native-worklets/plugin` babel
|
|
1356
|
+
// transform lifts those into wrapper functions carrying `__closure`,
|
|
1357
|
+
// `__workletHash`, and `__initData`. without that transform,
|
|
1358
|
+
// `useAnimatedStyle`'s `let inputs = Object.values(updater.__closure ?? {})`
|
|
1359
|
+
// returns `[]` and the mapper never subscribes — animations don't run.
|
|
1360
|
+
//
|
|
1361
|
+
// metro builds (apps loaded inside sootsim — 3pc, bluesky, expensify,
|
|
1362
|
+
// eigen, …) run the plugin in their own pipeline, so the bundle that
|
|
1363
|
+
// reaches us already has `__closure` baked in. our transform only needs
|
|
1364
|
+
// to act on:
|
|
1365
|
+
//
|
|
1366
|
+
// - sootsim's own src + test fixtures + external-app source, and
|
|
1367
|
+
// - upstream library source loaded from node_modules where the redirect
|
|
1368
|
+
// plugin let pure-JS resolve naturally.
|
|
1369
|
+
//
|
|
1370
|
+
// the keyword list + ignored-path list is ported from
|
|
1371
|
+
// `~/one/packages/compiler/src/transformBabel.ts` (which is itself ported
|
|
1372
|
+
// from reanimated 3.15.1's autoworkletization keywords). keep the two in
|
|
1373
|
+
// sync if upstream adds new auto-workletized hooks. detection is
|
|
1374
|
+
// content-driven, not path-driven, so we don't need a per-package
|
|
1375
|
+
// allow-list — anything that mentions a workletizable identifier and
|
|
1376
|
+
// isn't on the ignored-path list goes through.
|
|
1377
|
+
|
|
1378
|
+
export function workletsBabelTransform(
|
|
1379
|
+
// accepted for future per-target overrides; matching is content-driven
|
|
1380
|
+
// today so we don't actually consult it. kept for plugin-list parity.
|
|
1381
|
+
_isAppSource: (p: string) => boolean = () => false,
|
|
1382
|
+
): Plugin {
|
|
1383
|
+
return {
|
|
1384
|
+
name: 'sootsim-worklets-babel-transform',
|
|
1385
|
+
enforce: 'pre',
|
|
1386
|
+
transform: {
|
|
1387
|
+
filter: {
|
|
1388
|
+
id: {
|
|
1389
|
+
include: /\.(?:tsx?|jsx?|mjs|cjs|mts|cts)$/,
|
|
1390
|
+
exclude: /node_modules\/(?:react|react-dom|react-native|react-native-web)\//,
|
|
1391
|
+
},
|
|
1392
|
+
code: REANIMATED_WORKLETIZATION_REGEX,
|
|
1393
|
+
},
|
|
1394
|
+
async handler(code, id) {
|
|
1395
|
+
if (!shouldApplyWorkletsPlugin(id, code)) return null
|
|
1396
|
+
try {
|
|
1397
|
+
if (process.env.SOOTSIM_WORKLETS_BABEL_DEBUG) {
|
|
1398
|
+
console.log('[worklets-babel] transforming', id.slice(-80))
|
|
1399
|
+
}
|
|
1400
|
+
return await transformWorkletsCode(code, id, true)
|
|
1401
|
+
} catch (err) {
|
|
1402
|
+
// surface but don't kill the build — fall back to raw code so the
|
|
1403
|
+
// rest of the pipeline can run. these failures are loud in the
|
|
1404
|
+
// console and easy to chase.
|
|
1405
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
1406
|
+
console.warn(`[sootsim-worklets-babel] transform failed for ${id}: ${message}`)
|
|
1407
|
+
return null
|
|
1408
|
+
}
|
|
1409
|
+
},
|
|
1410
|
+
},
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// rolldown can't parse JSX in .js files — transform them first.
|
|
1415
|
+
// needed because some RN packages (e.g. react-native-actions-sheet) ship JSX in .js.
|
|
1416
|
+
// some upstream RN libraries (e.g. @react-native-segmented-control/segmented-control)
|
|
1417
|
+
// publish raw Flow source as their runtime entry. Metro strips Flow in its babel
|
|
1418
|
+
// pipeline; vite/rolldown/oxc do not. detect the `@flow` pragma in node_modules and
|
|
1419
|
+
// run @babel/plugin-transform-flow-strip-types so the wrapper-real seam can pull
|
|
1420
|
+
// in the real upstream JS without re-implementing it locally. mirrored in
|
|
1421
|
+
// scripts/build-bundler-deps.ts so esbuild-based dep prebundles stay in sync.
|
|
1422
|
+
let flowBabel: typeof import('@babel/core') | null = null
|
|
1423
|
+
let flowStripPlugin: import('@babel/core').PluginItem | null = null
|
|
1424
|
+
let jsxSyntaxPlugin: import('@babel/core').PluginItem | null = null
|
|
1425
|
+
function loadFlowBabel(): void {
|
|
1426
|
+
if (flowBabel) return
|
|
1427
|
+
flowBabel = sootsimPluginRequire('@babel/core')
|
|
1428
|
+
flowStripPlugin = sootsimPluginRequire(
|
|
1429
|
+
'@babel/plugin-transform-flow-strip-types',
|
|
1430
|
+
) as import('@babel/core').PluginItem
|
|
1431
|
+
jsxSyntaxPlugin = sootsimPluginRequire(
|
|
1432
|
+
'@babel/plugin-syntax-jsx',
|
|
1433
|
+
) as import('@babel/core').PluginItem
|
|
1434
|
+
}
|
|
1435
|
+
function flowStripTransform(): Plugin {
|
|
1436
|
+
return {
|
|
1437
|
+
name: 'sootsim-flow-strip',
|
|
1438
|
+
enforce: 'pre',
|
|
1439
|
+
transform: {
|
|
1440
|
+
filter: {
|
|
1441
|
+
id: /\/node_modules\/.*\.[cm]?jsx?$/,
|
|
1442
|
+
code: /@flow/,
|
|
1443
|
+
},
|
|
1444
|
+
handler(code, id) {
|
|
1445
|
+
loadFlowBabel()
|
|
1446
|
+
const result = flowBabel!.transformSync(code, {
|
|
1447
|
+
filename: id,
|
|
1448
|
+
babelrc: false,
|
|
1449
|
+
configFile: false,
|
|
1450
|
+
plugins: [flowStripPlugin!, jsxSyntaxPlugin!],
|
|
1451
|
+
sourceMaps: true,
|
|
1452
|
+
compact: false,
|
|
1453
|
+
})
|
|
1454
|
+
if (!result?.code) return null
|
|
1455
|
+
return { code: result.code, map: result.map ?? null }
|
|
1456
|
+
},
|
|
1457
|
+
},
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function nodeModulesJsxTransform(): Plugin {
|
|
1462
|
+
return {
|
|
1463
|
+
name: 'sootsim-node-modules-jsx',
|
|
1464
|
+
enforce: 'pre',
|
|
1465
|
+
transform: {
|
|
1466
|
+
filter: {
|
|
1467
|
+
id: /\/node_modules\/.*\.js$/,
|
|
1468
|
+
code: /</,
|
|
1469
|
+
},
|
|
1470
|
+
async handler(code, id) {
|
|
1471
|
+
// quick check: does it look like JSX? (angle bracket followed by uppercase or known RN component)
|
|
1472
|
+
if (!/[=(\s,]<[A-Z]/.test(code) && !/<\/[A-Z]/.test(code)) return null
|
|
1473
|
+
const needsReact =
|
|
1474
|
+
!code.includes('import React') && !code.includes('import * as React')
|
|
1475
|
+
const withReact = needsReact ? `import React from 'react';\n${code}` : code
|
|
1476
|
+
const transformed = await transformWithOxc(withReact, id, {
|
|
1477
|
+
lang: 'jsx',
|
|
1478
|
+
jsx: { runtime: 'classic' },
|
|
1479
|
+
})
|
|
1480
|
+
return { code: transformed.code, map: transformed.map }
|
|
1481
|
+
},
|
|
1482
|
+
},
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// metro-style platform resolution: .ios > .native > base
|
|
1487
|
+
function metroNativeResolve(isAppSource: (p: string) => boolean): Plugin {
|
|
1488
|
+
const platformExts = [
|
|
1489
|
+
'.ios.tsx',
|
|
1490
|
+
'.ios.ts',
|
|
1491
|
+
'.ios.jsx',
|
|
1492
|
+
'.ios.js',
|
|
1493
|
+
'.native.tsx',
|
|
1494
|
+
'.native.ts',
|
|
1495
|
+
'.native.jsx',
|
|
1496
|
+
'.native.js',
|
|
1497
|
+
'.native.mjs',
|
|
1498
|
+
]
|
|
1499
|
+
const allExts = [...platformExts, '.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs']
|
|
1500
|
+
|
|
1501
|
+
function tryResolve(base: string): string | null {
|
|
1502
|
+
for (const ext of platformExts) {
|
|
1503
|
+
const candidate = base + ext
|
|
1504
|
+
try {
|
|
1505
|
+
if (fs.existsSync(candidate)) return candidate
|
|
1506
|
+
} catch {}
|
|
1507
|
+
}
|
|
1508
|
+
for (const ext of platformExts) {
|
|
1509
|
+
const candidate = path.join(base, 'index' + ext)
|
|
1510
|
+
try {
|
|
1511
|
+
if (fs.existsSync(candidate)) return candidate
|
|
1512
|
+
} catch {}
|
|
1513
|
+
}
|
|
1514
|
+
return null
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
return {
|
|
1518
|
+
name: 'sootsim-metro-native-resolve',
|
|
1519
|
+
enforce: 'pre',
|
|
1520
|
+
resolveId: {
|
|
1521
|
+
filter: { id: /^\./ },
|
|
1522
|
+
handler(source, importer) {
|
|
1523
|
+
if (!importer) return null
|
|
1524
|
+
if (!importer.includes('node_modules') && !isAppSource(importer)) return null
|
|
1525
|
+
if (platformExts.some((ext) => source.endsWith(ext))) return null
|
|
1526
|
+
|
|
1527
|
+
const dir = path.dirname(importer)
|
|
1528
|
+
|
|
1529
|
+
const hasBaseExt = allExts.some((ext) => source.endsWith(ext))
|
|
1530
|
+
if (hasBaseExt) {
|
|
1531
|
+
const resolved = path.resolve(dir, source)
|
|
1532
|
+
for (const ext of allExts) {
|
|
1533
|
+
if (source.endsWith(ext)) {
|
|
1534
|
+
const found = tryResolve(resolved.slice(0, -ext.length))
|
|
1535
|
+
if (found) return found
|
|
1536
|
+
break
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
} else {
|
|
1540
|
+
const found = tryResolve(path.resolve(dir, source))
|
|
1541
|
+
if (found) return found
|
|
1542
|
+
}
|
|
1543
|
+
return null
|
|
1544
|
+
},
|
|
1545
|
+
},
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// convert require("react-native") and deep library requires to ESM imports
|
|
1550
|
+
function reactNativeRequirePlugin(isAppSource: (p: string) => boolean): Plugin {
|
|
1551
|
+
return {
|
|
1552
|
+
name: 'sootsim-react-native-require',
|
|
1553
|
+
enforce: 'pre',
|
|
1554
|
+
transform: {
|
|
1555
|
+
filter: { code: /react-native/ },
|
|
1556
|
+
handler(code, id) {
|
|
1557
|
+
if (!id.includes('node_modules') && !isAppSource(id)) return null
|
|
1558
|
+
|
|
1559
|
+
let hasChanges = false
|
|
1560
|
+
let imports = ''
|
|
1561
|
+
let result = code
|
|
1562
|
+
let importCounter = 0
|
|
1563
|
+
|
|
1564
|
+
if (
|
|
1565
|
+
result.includes('require("react-native")') ||
|
|
1566
|
+
result.includes("require('react-native')")
|
|
1567
|
+
) {
|
|
1568
|
+
imports += `import * as __soot_rn_shim__ from ${JSON.stringify(rnShimPath)};\n`
|
|
1569
|
+
result = result.replace(/require\(["']react-native["']\)/g, '__soot_rn_shim__')
|
|
1570
|
+
hasChanges = true
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
const deepRequireRegex = /require\(["'](react-native\/Libraries\/[^"']+)["']\)/g
|
|
1574
|
+
const replacements: Array<{ full: string; path: string; varName: string }> = []
|
|
1575
|
+
const seenPaths = new Map<string, string>()
|
|
1576
|
+
let match
|
|
1577
|
+
|
|
1578
|
+
deepRequireRegex.lastIndex = 0
|
|
1579
|
+
while ((match = deepRequireRegex.exec(result)) !== null) {
|
|
1580
|
+
const importPath = match[1]
|
|
1581
|
+
if (!seenPaths.has(importPath)) {
|
|
1582
|
+
const varName = `__soot_rn_lib_${importCounter++}__`
|
|
1583
|
+
const stubPath = rnLibraryStubs[importPath] || rnDeepStubDefault
|
|
1584
|
+
imports += `import * as ${varName} from ${JSON.stringify(stubPath)};\n`
|
|
1585
|
+
seenPaths.set(importPath, varName)
|
|
1586
|
+
}
|
|
1587
|
+
replacements.push({
|
|
1588
|
+
full: match[0],
|
|
1589
|
+
path: importPath,
|
|
1590
|
+
varName: seenPaths.get(importPath)!,
|
|
1591
|
+
})
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
if (replacements.length > 0) {
|
|
1595
|
+
for (const rep of replacements) {
|
|
1596
|
+
result = result.replace(rep.full, rep.varName)
|
|
1597
|
+
}
|
|
1598
|
+
hasChanges = true
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
if (!hasChanges) return null
|
|
1602
|
+
return { code: imports + result, map: null }
|
|
1603
|
+
},
|
|
1604
|
+
},
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// resolve bare imports from external app's node_modules
|
|
1609
|
+
function externalAppResolvePlugin(
|
|
1610
|
+
appDir: string,
|
|
1611
|
+
isAppSource: (p: string) => boolean,
|
|
1612
|
+
ownedPackages: Set<string>,
|
|
1613
|
+
): Plugin {
|
|
1614
|
+
// synthetic importer inside the app dir so vite searches app's node_modules
|
|
1615
|
+
// using its conditions-aware exports resolution (not CJS require.resolve)
|
|
1616
|
+
const appVirtualImporter = path.join(appDir, '_virtual_.js')
|
|
1617
|
+
|
|
1618
|
+
return {
|
|
1619
|
+
name: 'sootsim-external-app-resolve',
|
|
1620
|
+
enforce: 'pre',
|
|
1621
|
+
async resolveId(source, importer, options) {
|
|
1622
|
+
if (!importer) return null
|
|
1623
|
+
if (source.startsWith('.') || source.startsWith('/') || source.startsWith('\0'))
|
|
1624
|
+
return null
|
|
1625
|
+
if (ownedPackages.has(source) || ownedPackages.has(getPackageName(source)))
|
|
1626
|
+
return null
|
|
1627
|
+
if (!isAppSource(importer) && !importer.includes(path.join(appDir, 'node_modules')))
|
|
1628
|
+
return null
|
|
1629
|
+
if (source === 'react-native' || source.startsWith('react-native/')) return null
|
|
1630
|
+
|
|
1631
|
+
// re-resolve through vite's pipeline so exports conditions (react-native, import) are respected
|
|
1632
|
+
const resolved = await this.resolve(source, appVirtualImporter, {
|
|
1633
|
+
...options,
|
|
1634
|
+
skipSelf: true,
|
|
1635
|
+
})
|
|
1636
|
+
return resolved || null
|
|
1637
|
+
},
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// convert require() calls and handle JSX in .js files from external app sources
|
|
1642
|
+
function externalAppTransformPlugin(isAppSource: (p: string) => boolean): Plugin {
|
|
1643
|
+
return {
|
|
1644
|
+
name: 'sootsim-external-app-transform',
|
|
1645
|
+
enforce: 'pre',
|
|
1646
|
+
transform: {
|
|
1647
|
+
filter: { code: /require\(|</ },
|
|
1648
|
+
async handler(code, id) {
|
|
1649
|
+
if (!isAppSource(id)) return null
|
|
1650
|
+
|
|
1651
|
+
let result = code
|
|
1652
|
+
let imports = ''
|
|
1653
|
+
let importCounter = 0
|
|
1654
|
+
let hasChanges = false
|
|
1655
|
+
|
|
1656
|
+
if (code.includes('require(')) {
|
|
1657
|
+
const requireRegex = /require\(["']([^"']+)["']\)/g
|
|
1658
|
+
const replacements: Array<{ full: string; varName: string }> = []
|
|
1659
|
+
const seenPaths = new Map<string, string>()
|
|
1660
|
+
let match
|
|
1661
|
+
|
|
1662
|
+
requireRegex.lastIndex = 0
|
|
1663
|
+
while ((match = requireRegex.exec(result)) !== null) {
|
|
1664
|
+
const reqPath = match[1]
|
|
1665
|
+
if (reqPath === 'react-native' || reqPath.startsWith('react-native/'))
|
|
1666
|
+
continue
|
|
1667
|
+
|
|
1668
|
+
if (!seenPaths.has(reqPath)) {
|
|
1669
|
+
const varName = `__soot_cjs_${importCounter++}__`
|
|
1670
|
+
imports += `import * as ${varName} from ${JSON.stringify(reqPath)};\n`
|
|
1671
|
+
seenPaths.set(reqPath, varName)
|
|
1672
|
+
}
|
|
1673
|
+
replacements.push({ full: match[0], varName: seenPaths.get(reqPath)! })
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (replacements.length > 0) {
|
|
1677
|
+
for (const rep of replacements) {
|
|
1678
|
+
result = result.replace(rep.full, rep.varName)
|
|
1679
|
+
}
|
|
1680
|
+
hasChanges = true
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
if (imports) result = imports + result
|
|
1685
|
+
|
|
1686
|
+
if (id.endsWith('.js') && result.includes('<')) {
|
|
1687
|
+
const needsReact =
|
|
1688
|
+
!result.includes('import React') && !result.includes('import * as React')
|
|
1689
|
+
const withReact = needsReact ? `import React from 'react';\n${result}` : result
|
|
1690
|
+
const transformed = await transformWithOxc(withReact, id, {
|
|
1691
|
+
lang: 'jsx',
|
|
1692
|
+
jsx: { runtime: 'classic' },
|
|
1693
|
+
})
|
|
1694
|
+
return { code: transformed.code, map: transformed.map }
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
if (!hasChanges) return null
|
|
1698
|
+
return { code: result, map: null }
|
|
1699
|
+
},
|
|
1700
|
+
},
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// auto-stub native packages that don't exist in any node_modules
|
|
1705
|
+
function stubMissingNativeDeps(appDir: string): Plugin {
|
|
1706
|
+
const checked = new Map<string, boolean>()
|
|
1707
|
+
|
|
1708
|
+
function packageExists(pkgName: string): boolean {
|
|
1709
|
+
const cached = checked.get(pkgName)
|
|
1710
|
+
if (cached !== undefined) return cached
|
|
1711
|
+
let exists = moduleExistsIn(pkgName, sootsimRoot)
|
|
1712
|
+
if (!exists && appDir) exists = moduleExistsIn(pkgName, appDir)
|
|
1713
|
+
checked.set(pkgName, exists)
|
|
1714
|
+
return exists
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
return {
|
|
1718
|
+
name: 'sootsim-stub-missing-native-deps',
|
|
1719
|
+
resolveId(source) {
|
|
1720
|
+
if (!isNativePackage(source)) return null
|
|
1721
|
+
const pkgName = getPackageName(source)
|
|
1722
|
+
if (packageExists(pkgName)) return null
|
|
1723
|
+
console.log(`[rnx] auto-stubbing missing native dep: ${source}`)
|
|
1724
|
+
return nativeAutoStubPath
|
|
1725
|
+
},
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// stub missing image imports from node_modules
|
|
1730
|
+
function stubMissingImages(isAppSource: (p: string) => boolean): Plugin {
|
|
1731
|
+
return {
|
|
1732
|
+
name: 'sootsim-stub-images',
|
|
1733
|
+
enforce: 'pre',
|
|
1734
|
+
resolveId: {
|
|
1735
|
+
filter: { id: /\.(?:jpg|png|gif)$/ },
|
|
1736
|
+
handler(id, importer) {
|
|
1737
|
+
if (importer?.includes('node_modules') || (importer && isAppSource(importer))) {
|
|
1738
|
+
return '\0stub-image'
|
|
1739
|
+
}
|
|
1740
|
+
return null
|
|
1741
|
+
},
|
|
1742
|
+
},
|
|
1743
|
+
load: {
|
|
1744
|
+
filter: { id: new RegExp(`^${virtualIdPrefix}stub-image$`) },
|
|
1745
|
+
handler() {
|
|
1746
|
+
return 'export default ""'
|
|
1747
|
+
},
|
|
1748
|
+
},
|
|
1749
|
+
}
|
|
1750
|
+
}
|