sootsim 0.1.83 → 0.1.85
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/README.md +0 -1
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +36 -0
- package/detox/expectations.ts +477 -0
- package/detox/gestures.ts +442 -0
- package/detox/index.ts +1403 -0
- package/detox/jest-environment.ts +86 -0
- package/detox/jest-preset.cjs +50 -0
- package/detox/matchers.ts +29 -0
- package/detox/navigation.ts +43 -0
- package/detox/run-test.ts +113 -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 +3 -3
- package/dist-cli/chunks/{agent-MQ7GLVIB.js → agent-T3DUH5YJ.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-7KAFDQCN.js → agent-wrapper-NSBF4THI.js} +2 -2
- package/dist-cli/chunks/{assert-TV46GUNU.js → assert-X3F7TRCZ.js} +2 -2
- package/dist-cli/chunks/auto-bootstrap-47RN2V5G.js +2 -0
- package/dist-cli/chunks/beta-BRCGAF2N.js +2 -0
- package/dist-cli/chunks/chunk-36RPD6JI.js +2 -0
- package/dist-cli/chunks/{chunk-PM5NVKLP.js → chunk-3WGHC7JN.js} +2 -2
- package/dist-cli/chunks/chunk-4DBPNLGI.js +1 -0
- package/dist-cli/chunks/{chunk-J2GYISVJ.js → chunk-4EVSIUNB.js} +2 -2
- package/dist-cli/chunks/{chunk-JHJNODXN.js → chunk-4QZHZ6BC.js} +2 -2
- package/dist-cli/chunks/{chunk-F3HP444U.js → chunk-5DIGWOY7.js} +1 -1
- package/dist-cli/chunks/{chunk-DP7O5MHK.js → chunk-5N3V7OCG.js} +2 -2
- package/dist-cli/chunks/{chunk-Y4BUVURT.js → chunk-5S6D7K4L.js} +2 -2
- package/dist-cli/chunks/{chunk-ECJBV65H.js → chunk-7LKUN46F.js} +2 -2
- package/dist-cli/chunks/{chunk-WTKTOL3C.js → chunk-AC6QGW22.js} +2 -2
- package/dist-cli/chunks/{chunk-IBNRRAES.js → chunk-AFNDVS4E.js} +2 -2
- package/dist-cli/chunks/{chunk-6TNANCQC.js → chunk-BESAZ2HA.js} +2 -2
- package/dist-cli/chunks/{chunk-WN7M3QON.js → chunk-BHZJ6RIH.js} +2 -2
- package/dist-cli/chunks/{chunk-277XAALA.js → chunk-BZL6D4TV.js} +3 -3
- package/dist-cli/chunks/{chunk-CYV6Y6YV.js → chunk-CF2LPRXD.js} +2 -2
- package/dist-cli/chunks/chunk-DWTLRPEN.js +79 -0
- package/dist-cli/chunks/{chunk-CJY3AVI7.js → chunk-E2QE5FFP.js} +1 -1
- package/dist-cli/chunks/chunk-EBEL6TTJ.js +4 -0
- package/dist-cli/chunks/{chunk-DM6WT7QM.js → chunk-EFM53PZ5.js} +1 -1
- package/dist-cli/chunks/{chunk-YUELRHGB.js → chunk-EKXK3SWK.js} +2 -2
- package/dist-cli/chunks/{chunk-4LS5MZAI.js → chunk-G7CIZ5S3.js} +3 -3
- package/dist-cli/chunks/{chunk-6NN2D4EJ.js → chunk-GTAD6IUV.js} +1 -1
- package/dist-cli/chunks/{chunk-OYMFNU3M.js → chunk-H44IQHKZ.js} +1 -1
- package/dist-cli/chunks/{chunk-IP3QJLRH.js → chunk-HQDJ5BOF.js} +1 -1
- package/dist-cli/chunks/{chunk-5DJXZIFZ.js → chunk-KUSQ4NNJ.js} +1 -1
- package/dist-cli/chunks/{chunk-HAWOAQAG.js → chunk-MAO7F5PH.js} +3 -3
- package/dist-cli/chunks/{chunk-572VSFNP.js → chunk-NVTL3JQG.js} +1 -1
- package/dist-cli/chunks/{chunk-6XZOEBTZ.js → chunk-O6N2CEET.js} +2 -2
- package/dist-cli/chunks/{chunk-HNWEELAE.js → chunk-OISHLFON.js} +1 -1
- package/dist-cli/chunks/{chunk-2PY3UZVO.js → chunk-OUNLJM56.js} +2 -2
- package/dist-cli/chunks/chunk-OXOARRKR.js +67 -0
- package/dist-cli/chunks/{chunk-NXATOWWF.js → chunk-PHPXGLME.js} +1 -1
- package/dist-cli/chunks/{chunk-JQ7ZXOXJ.js → chunk-PQFFUJR6.js} +2 -2
- package/dist-cli/chunks/{chunk-KASUZ5XV.js → chunk-QLJNSOS7.js} +1 -1
- package/dist-cli/chunks/chunk-QQAECG5B.js +2 -0
- package/dist-cli/chunks/{chunk-FJYT7XL2.js → chunk-RZHREO3M.js} +2 -2
- package/dist-cli/chunks/{chunk-FRM355UL.js → chunk-SBGOUA6F.js} +2 -2
- package/dist-cli/chunks/chunk-SSCA2AEA.js +1 -0
- package/dist-cli/chunks/{chunk-Y2VJBRSP.js → chunk-UYRGCJ4N.js} +1 -1
- package/dist-cli/chunks/{chunk-2AWQ7OB2.js → chunk-WGDL5V6C.js} +1 -1
- package/dist-cli/chunks/{chunk-VMXWC2JO.js → chunk-Y5PLPEEU.js} +2 -2
- package/dist-cli/chunks/chunk-ZFAM4N5B.js +1 -0
- package/dist-cli/chunks/{chunk-RH4F2TF7.js → chunk-ZO3VHP6W.js} +1 -1
- package/dist-cli/chunks/cli-version-WPFDM2A6.js +2 -0
- package/dist-cli/chunks/{compat-QLLWBTS3.js → compat-PCXGGZBZ.js} +3 -3
- package/dist-cli/chunks/{config-2DSLDCXV.js → config-LULEVEYL.js} +2 -2
- package/dist-cli/chunks/control-6P6HY7UF.js +2 -0
- package/dist-cli/chunks/{cpu-profile-GEIKHCPC.js → cpu-profile-NOK73ZYW.js} +2 -2
- package/dist-cli/chunks/{daemon-4EBUFN4D.js → daemon-4A3DMUYL.js} +2 -2
- package/dist-cli/chunks/{debug-WGD6XWOF.js → debug-74BWB2ZG.js} +3 -3
- package/dist-cli/chunks/{detox-LNKGRZU6.js → detox-HEOMINSC.js} +2 -2
- package/dist-cli/chunks/{device-AYKXKVIQ.js → device-TTXXBJFZ.js} +2 -2
- package/dist-cli/chunks/{diagnose-TMXSDOOC.js → diagnose-QZ3GOHSE.js} +2 -2
- package/dist-cli/chunks/drivers-QRPWNOIT.js +2 -0
- package/dist-cli/chunks/{electron-QFPF7TBY.js → electron-QVOWV44R.js} +3 -3
- package/dist-cli/chunks/flow-QMA7GVN6.js +2 -0
- package/dist-cli/chunks/{hints-MXKRR4TG.js → hints-YKWRNMJC.js} +2 -2
- package/dist-cli/chunks/{home-paths-REMWQDAO.js → home-paths-SFADSTJM.js} +2 -2
- package/dist-cli/chunks/{inspect-XGSQNFV7.js → inspect-LEWGQCIU.js} +3 -3
- package/dist-cli/chunks/install-7N2N7Q32.js +2 -0
- package/dist-cli/chunks/{install-desktop-NQG3RZSA.js → install-desktop-22HYQZ2G.js} +3 -3
- package/dist-cli/chunks/{keys-5QZWXL3F.js → keys-3ZT3MICU.js} +2 -2
- package/dist-cli/chunks/{launch-SBXOZWKO.js → launch-ZXW2NFLG.js} +3 -3
- package/dist-cli/chunks/{login-EACQXE24.js → login-NJKJ7GZO.js} +4 -4
- package/dist-cli/chunks/{logout-IBQLMUML.js → logout-VMMQL7CB.js} +2 -2
- package/dist-cli/chunks/{maestro-LFYXUX7O.js → maestro-OJY4MTI7.js} +2 -2
- package/dist-cli/chunks/{preview-U4SBOEGQ.js → preview-QU2GXTEV.js} +2 -2
- package/dist-cli/chunks/{profile-GWS5ECMY.js → profile-7APWK47T.js} +2 -2
- package/dist-cli/chunks/{react-QDHLMVYL.js → react-RSVO5JZZ.js} +2 -2
- package/dist-cli/chunks/{record-BUEUWPDI.js → record-UWH4MDEO.js} +2 -2
- package/dist-cli/chunks/runtime-3FUENRHM.js +2 -0
- package/dist-cli/chunks/{runtime-delivery-G7L6RVZ7.js → runtime-delivery-QMKGRV7N.js} +2 -2
- package/dist-cli/chunks/{screenshot-T2HBA3VI.js → screenshot-43M27ALE.js} +2 -2
- package/dist-cli/chunks/{screenshot-mode-EG5HMIH3.js → screenshot-mode-EBYYN6TY.js} +2 -2
- package/dist-cli/chunks/{screenshots-S52AFHTV.js → screenshots-7TQZL6Z6.js} +2 -2
- package/dist-cli/chunks/{server-MFFVYUGG.js → server-VCFM25Z6.js} +2 -2
- package/dist-cli/chunks/setup-repo-HFH4VKJQ.js +2 -0
- package/dist-cli/chunks/{skills-HQGWBS2O.js → skills-RQA6EJQL.js} +2 -2
- package/dist-cli/chunks/{start-E3DRYY7W.js → start-ZT6MBYND.js} +4 -4
- package/dist-cli/chunks/store-BJBTDSZE.js +2 -0
- package/dist-cli/chunks/telemetry-ZZZKTILZ.js +2 -0
- package/dist-cli/chunks/{test-ZY3EF62K.js → test-RNRX5SWV.js} +3 -3
- package/dist-cli/chunks/{three-mode-WSPKQCJ5.js → three-mode-TQZH25ZO.js} +2 -2
- package/dist-cli/chunks/{timeline-3XAB5EWZ.js → timeline-GGN3AY6P.js} +2 -2
- package/dist-cli/chunks/{upgrade-WNENPFM5.js → upgrade-XT22D67C.js} +2 -2
- package/dist-cli/chunks/upload-NC2AYLC5.js +2 -0
- package/dist-cli/chunks/{web-D2AOZY44.js → web-KEHVF5MB.js} +2 -2
- package/dist-cli/chunks/{what-happened-F43KNSG6.js → what-happened-PATQRJ5T.js} +2 -2
- package/dist-cli/chunks/{whoami-T22VBR7C.js → whoami-CXVY26VV.js} +2 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +44 -0
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1770 -0
- package/dist-lib/detox/jest-preset.cjs +50 -0
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/index.cjs +136 -138
- package/dist-lib/metro.cjs +31 -26
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/demo-app-registry.cjs +809 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
- package/dist-lib/skills.cjs +17766 -0
- package/dist-lib/vite.cjs +129 -39
- package/package.json +39 -14
- package/scripts/demo-app-registry.ts +989 -0
- package/scripts/dev-server-scanner.ts +674 -0
- package/src/agent-daemon-client.ts +390 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +572 -0
- package/src/attached-projects.ts +536 -0
- package/src/auth/shared-session.ts +199 -0
- package/src/backend-origin.ts +49 -0
- package/src/beta.ts +21 -0
- package/src/bridge-constants.ts +10 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +30 -0
- package/src/codex-client.ts +215 -0
- package/src/config.ts +110 -0
- package/src/dev-bundle-resolution.ts +180 -0
- package/src/home-paths.ts +382 -0
- package/src/host/agent-host.ts +576 -0
- package/src/host/bridge-host.ts +2293 -0
- package/src/host/fetch-proxy-handler.ts +288 -0
- package/src/host/fetch-proxy-overrides.ts +39 -0
- package/src/host/open-url.ts +234 -0
- package/src/index.ts +9 -0
- package/src/metro-plugin.ts +139 -0
- package/src/native-dev-bundle-url.ts +62 -0
- package/src/native-seam-manifest.ts +313 -0
- package/src/profiles.ts +179 -0
- package/src/render-mode.ts +27 -0
- package/src/runtime-assets.ts +84 -0
- package/src/runtime-delivery.ts +334 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +438 -0
- package/src/screenshots/orchestrate.ts +244 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/skills/builtin/a11y-review.ts +126 -0
- package/src/skills/builtin/compat-check.ts +104 -0
- package/src/skills/builtin/perf-profile.ts +84 -0
- package/src/skills/builtin/screenshot-all.ts +46 -0
- package/src/skills/builtin/test-flow.ts +118 -0
- package/src/skills/builtin/visual-diff.ts +94 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/vite-plugin-one.ts +189 -0
- package/src/vite-plugin.ts +1381 -0
- package/src/worklets-babel.ts +132 -0
- package/dist-cli/chunks/auto-bootstrap-FQS4ZD2K.js +0 -2
- package/dist-cli/chunks/beta-VG7CDY2U.js +0 -2
- package/dist-cli/chunks/chunk-2OIBDYHW.js +0 -1
- package/dist-cli/chunks/chunk-6BNLVMXA.js +0 -1
- package/dist-cli/chunks/chunk-6XD6CBJM.js +0 -2
- package/dist-cli/chunks/chunk-CHQTO426.js +0 -1
- package/dist-cli/chunks/chunk-FAPYGVIU.js +0 -4
- package/dist-cli/chunks/chunk-PEHFE3LG.js +0 -64
- package/dist-cli/chunks/chunk-RXH2SLKF.js +0 -2
- package/dist-cli/chunks/chunk-UXQWC5ZR.js +0 -79
- package/dist-cli/chunks/chunk-XFQL74PF.js +0 -5
- package/dist-cli/chunks/cli-version-PWF6I6LY.js +0 -2
- package/dist-cli/chunks/control-UIOXGYXU.js +0 -2
- package/dist-cli/chunks/demo-app-registry-G3BDOFWC.js +0 -2
- package/dist-cli/chunks/drivers-IDQF34HP.js +0 -2
- package/dist-cli/chunks/flow-3JN3Y7RF.js +0 -2
- package/dist-cli/chunks/install-2N3YOOSN.js +0 -2
- package/dist-cli/chunks/runtime-PVB4VGUH.js +0 -2
- package/dist-cli/chunks/setup-repo-YOF7NV5D.js +0 -2
- package/dist-cli/chunks/store-MAI6D3UO.js +0 -2
- package/dist-cli/chunks/telemetry-RCQKCJTH.js +0 -2
- package/dist-cli/chunks/upload-YLJ4RA73.js +0 -2
- package/dist-lib/vite-base.cjs +0 -6937
|
@@ -0,0 +1,1381 @@
|
|
|
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 {
|
|
17
|
+
SOOTSIM_COMPAT_WRAPPER_REAL_PACKAGES,
|
|
18
|
+
SOOTSIM_HAPTIC_FEEDBACK_TOUCHABLE_SOURCE,
|
|
19
|
+
SOOTSIM_HAPTIC_FEEDBACK_TOUCHABLE_SPECIFIER,
|
|
20
|
+
compatStubsForBuildResolver,
|
|
21
|
+
reactNativeDeepStubsForBuildResolver,
|
|
22
|
+
} from '../../compat/src/stub-manifest.ts'
|
|
23
|
+
import { DEFAULT_SOOTSIM_BRIDGE_PORT } from './bridge-constants.ts'
|
|
24
|
+
import { shouldApplyWorkletsPlugin, transformWorkletsCode } from './worklets-babel.ts'
|
|
25
|
+
|
|
26
|
+
const sootsimPluginRequire = createRequire(import.meta.url)
|
|
27
|
+
|
|
28
|
+
export interface SootOptions {
|
|
29
|
+
// directory of an external RN app to load
|
|
30
|
+
app?: string
|
|
31
|
+
// additional source directories to treat as app sources (metro transforms apply)
|
|
32
|
+
sources?: string[]
|
|
33
|
+
// additional packages that must resolve from the host project, not the external app
|
|
34
|
+
ownedPackages?: string[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// derive sootsim's root from this file's location (src/vite-plugin.ts → ..)
|
|
38
|
+
const sootsimRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
39
|
+
const workspaceRoot = path.resolve(sootsimRoot, '..', '..')
|
|
40
|
+
const workspaceNodeModules = path.resolve(workspaceRoot, 'node_modules')
|
|
41
|
+
const workspaceTamaguiDir = path.resolve(workspaceNodeModules, '@tamagui')
|
|
42
|
+
// stubs and the mutable React bridge live in @soot/compat.
|
|
43
|
+
const compatRoot = path.resolve(sootsimRoot, '..', 'compat')
|
|
44
|
+
|
|
45
|
+
// the rendering engine and its shims live in the private sootsim-engine
|
|
46
|
+
// workspace package. resolve via workspace root rather than relative hops so
|
|
47
|
+
// renames of either package don't break things.
|
|
48
|
+
const engineRoot = path.resolve(workspaceRoot, 'packages/sootsim-engine')
|
|
49
|
+
const rnShimPath = path.resolve(engineRoot, 'src/react-native/index.ts')
|
|
50
|
+
const reactBridgePath = path.resolve(compatRoot, 'src/react-bridge.ts')
|
|
51
|
+
const sootsimBrowserSourceAliases = [
|
|
52
|
+
{
|
|
53
|
+
find: 'sootsim/backend-origin',
|
|
54
|
+
replacement: path.resolve(sootsimRoot, 'src/backend-origin.ts'),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
find: 'sootsim/bridge-constants',
|
|
58
|
+
replacement: path.resolve(sootsimRoot, 'src/bridge-constants.ts'),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
find: 'sootsim/dev-bundle-resolution',
|
|
62
|
+
replacement: path.resolve(sootsimRoot, 'src/dev-bundle-resolution.ts'),
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const compatStubsDir = path.resolve(compatRoot, 'src/stubs')
|
|
67
|
+
const nativeAutoStubPath = path.resolve(compatStubsDir, 'native-auto-stub.ts')
|
|
68
|
+
const rnDeepStubDefault = path.resolve(compatStubsDir, 'react-native-internals.ts')
|
|
69
|
+
|
|
70
|
+
function getInternalTamaguiPackages(): string[] {
|
|
71
|
+
try {
|
|
72
|
+
const scoped = fs
|
|
73
|
+
.readdirSync(workspaceTamaguiDir, { withFileTypes: true })
|
|
74
|
+
.filter((entry) => entry.isDirectory())
|
|
75
|
+
.map((entry) => `@tamagui/${entry.name}`)
|
|
76
|
+
|
|
77
|
+
return ['tamagui', ...scoped].sort()
|
|
78
|
+
} catch {
|
|
79
|
+
return ['tamagui']
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const internalTamaguiPackages = getInternalTamaguiPackages()
|
|
84
|
+
|
|
85
|
+
const vitePackageStubs = compatStubsForBuildResolver('vite')
|
|
86
|
+
const viteReactNativeDeepStubs = reactNativeDeepStubsForBuildResolver('vite')
|
|
87
|
+
const builtinStubs = Object.fromEntries(
|
|
88
|
+
vitePackageStubs.map((entry) => [entry.specifier, entry.stubFile]),
|
|
89
|
+
)
|
|
90
|
+
const rnLibraryStubs = Object.fromEntries(
|
|
91
|
+
viteReactNativeDeepStubs.map((entry) => [
|
|
92
|
+
entry.specifier,
|
|
93
|
+
path.resolve(compatStubsDir, entry.stubFile),
|
|
94
|
+
]),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
// resolve the real @react-navigation/native location at config time so the
|
|
98
|
+
// wrapper at compat/src/stubs/react-navigation-native.tsx can import it
|
|
99
|
+
// without bouncing back through the alias above. the wrapper imports from
|
|
100
|
+
// the synthetic specifier `@sootsim-internal/react-navigation-native-real`
|
|
101
|
+
// which is aliased to this absolute path; rolldown follows the path
|
|
102
|
+
// terminally so its `export *` static analysis sees the real export
|
|
103
|
+
// surface.
|
|
104
|
+
const reactNavigationNativeRealPath = (() => {
|
|
105
|
+
try {
|
|
106
|
+
return sootsimPluginRequire.resolve('@react-navigation/native')
|
|
107
|
+
} catch {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
})()
|
|
111
|
+
|
|
112
|
+
const reactNativeHapticFeedbackTouchablePath = (() => {
|
|
113
|
+
try {
|
|
114
|
+
return path.join(
|
|
115
|
+
path.dirname(
|
|
116
|
+
sootsimPluginRequire.resolve('react-native-haptic-feedback/package.json'),
|
|
117
|
+
),
|
|
118
|
+
SOOTSIM_HAPTIC_FEEDBACK_TOUCHABLE_SOURCE,
|
|
119
|
+
)
|
|
120
|
+
} catch {
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
})()
|
|
124
|
+
|
|
125
|
+
const compatWrapperRealPackageAliases: Array<{
|
|
126
|
+
find: string
|
|
127
|
+
replacement: string
|
|
128
|
+
}> = []
|
|
129
|
+
for (const { specifier, packageName } of Object.values(
|
|
130
|
+
SOOTSIM_COMPAT_WRAPPER_REAL_PACKAGES,
|
|
131
|
+
)) {
|
|
132
|
+
try {
|
|
133
|
+
compatWrapperRealPackageAliases.push({
|
|
134
|
+
find: specifier,
|
|
135
|
+
replacement: sootsimPluginRequire.resolve(packageName),
|
|
136
|
+
})
|
|
137
|
+
} catch {}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// packages that must always resolve from sootsim to ensure single instances
|
|
141
|
+
// across the renderer/runtime itself. keep this list minimal for external apps.
|
|
142
|
+
const coreOwnedPackages = [
|
|
143
|
+
'react',
|
|
144
|
+
'react-dom',
|
|
145
|
+
'react-reconciler',
|
|
146
|
+
'react/jsx-runtime',
|
|
147
|
+
'react/jsx-dev-runtime',
|
|
148
|
+
'canvaskit-wasm',
|
|
149
|
+
'canvaskit-wasm/full',
|
|
150
|
+
'yoga-layout',
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
const nativePackagePatterns = [
|
|
154
|
+
/^expo-/,
|
|
155
|
+
/^react-native-/,
|
|
156
|
+
/^@react-native\//,
|
|
157
|
+
/^@react-native-community\//,
|
|
158
|
+
/^@expo\//,
|
|
159
|
+
/^expo$/,
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
function getPackageName(source: string): string {
|
|
163
|
+
return source.startsWith('@')
|
|
164
|
+
? source.split('/').slice(0, 2).join('/')
|
|
165
|
+
: source.split('/')[0]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isNativePackage(source: string): boolean {
|
|
169
|
+
return nativePackagePatterns.some((p) => p.test(source))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function moduleExistsIn(pkgName: string, dir: string): boolean {
|
|
173
|
+
try {
|
|
174
|
+
return fs.existsSync(path.join(dir, 'node_modules', pkgName))
|
|
175
|
+
} catch {
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function sootsim(options: SootOptions = {}): Plugin[] {
|
|
181
|
+
const appDir = options.app ? path.resolve(options.app) : ''
|
|
182
|
+
const extraSources = (options.sources || []).map((s) => path.resolve(s))
|
|
183
|
+
const ownedPackages = new Set([...coreOwnedPackages, ...(options.ownedPackages || [])])
|
|
184
|
+
|
|
185
|
+
function isAppSource(filePath: string): boolean {
|
|
186
|
+
// exclude node_modules — only actual app source files
|
|
187
|
+
if (filePath.includes('/node_modules/')) return false
|
|
188
|
+
if (appDir && filePath.startsWith(appDir)) return true
|
|
189
|
+
return extraSources.some((s) => filePath.startsWith(s))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return [
|
|
193
|
+
wsBridgePlugin(),
|
|
194
|
+
reactBridgePlugin(),
|
|
195
|
+
sootsimConfigPlugin(appDir, extraSources),
|
|
196
|
+
fixJsxRuntimeExports(),
|
|
197
|
+
flowStripTransform(),
|
|
198
|
+
nodeModulesJsxTransform(),
|
|
199
|
+
// must run before metroNativeResolve — that plugin resolves relative
|
|
200
|
+
// imports like `./bindings` to absolute file paths via the platform-
|
|
201
|
+
// extension lookup, and once it returns a resolved id, downstream
|
|
202
|
+
// resolveId hooks don't see the source string anymore.
|
|
203
|
+
keyboardControllerBindingsRedirect(),
|
|
204
|
+
reanimatedNativeSeamRedirect(),
|
|
205
|
+
manifestNativeSeamRedirect(),
|
|
206
|
+
workletsBabelTransform(isAppSource),
|
|
207
|
+
metroNativeResolve(isAppSource),
|
|
208
|
+
reactNativeRequirePlugin(isAppSource),
|
|
209
|
+
externalAppResolvePlugin(appDir, isAppSource, ownedPackages),
|
|
210
|
+
externalAppTransformPlugin(isAppSource),
|
|
211
|
+
stubMissingNativeDeps(appDir),
|
|
212
|
+
stubMissingImages(isAppSource),
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// shared worker plugin chain for the engine's shell-host build (used by
|
|
217
|
+
// both `vite.config.ts` for dev/watch and `vite.build.config.ts` for prod).
|
|
218
|
+
// keep these here — duplicating the list inline drifts (which is exactly how
|
|
219
|
+
// the prod shell worker shipped without `reanimatedNativeSeamRedirect` and
|
|
220
|
+
// threw "Native part of Reanimated doesn't seem to be initialized" until
|
|
221
|
+
// 2e6a8450).
|
|
222
|
+
//
|
|
223
|
+
// shell-host.ts spawns shell-worker.ts via Vite's declarative
|
|
224
|
+
// `new Worker(new URL(...))` pattern, so shell-worker is a worker chunk of
|
|
225
|
+
// the engine build. apply react-bridge / jsx-runtime fixes plus the
|
|
226
|
+
// reanimated/keyboard-controller native-seam redirects so upstream's
|
|
227
|
+
// reanimated wires up our turbomodule + globals inside the worker too.
|
|
228
|
+
// without these, worker bundles see upstream's original
|
|
229
|
+
// `NativeReanimatedModule.ts`, `TurboModuleRegistry.get('ReanimatedModule')`
|
|
230
|
+
// returns null on web/sootsim, `__reanimatedModuleProxy` never gets set,
|
|
231
|
+
// and the NativeReanimated constructor throws on shell-worker mount.
|
|
232
|
+
//
|
|
233
|
+
// workletsBabelTransform populates `__closure` on user-code worklets so
|
|
234
|
+
// useAnimatedStyle/useDerivedValue/etc. subscribe correctly inside the
|
|
235
|
+
// worker. matching is content-driven today, so the predicate is `() => true`.
|
|
236
|
+
export function engineShellWorkerPlugins(): Plugin[] {
|
|
237
|
+
return [
|
|
238
|
+
workerReactBridgePlugin(),
|
|
239
|
+
fixJsxRuntimeExports(),
|
|
240
|
+
reanimatedNativeSeamRedirect(),
|
|
241
|
+
manifestNativeSeamRedirect(),
|
|
242
|
+
keyboardControllerBindingsRedirect(),
|
|
243
|
+
workletsBabelTransform(() => true),
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// react-native-keyboard-controller is pure-JS for everything except its
|
|
248
|
+
// native-event seam (`bindings.ts` / `bindings.native.ts`) and RN platform
|
|
249
|
+
// `findNodeHandle` resolution. let upstream's components, hooks, animated
|
|
250
|
+
// module, and KeyboardAvoidingView resolve from node_modules unchanged, and
|
|
251
|
+
// redirect only those native/platform seams.
|
|
252
|
+
//
|
|
253
|
+
// the redirect runs `enforce: 'pre'` so it sees `./bindings` / `../bindings`
|
|
254
|
+
// / `../../bindings` strings before vite's native resolver collapses them to
|
|
255
|
+
// absolute paths. matches by importer path (must be inside upstream's
|
|
256
|
+
// node_modules root) and source basename.
|
|
257
|
+
export function keyboardControllerBindingsRedirect(): Plugin {
|
|
258
|
+
const target = path.resolve(compatStubsDir, 'react-native-keyboard-controller.ts')
|
|
259
|
+
const findNodeHandleTarget = '\0sootsim:rnkc-find-node-handle-native'
|
|
260
|
+
const nativePlatformBasenames = new Set(['findNodeHandle', 'reanimated'])
|
|
261
|
+
const nativeExts = [
|
|
262
|
+
'.ios.tsx',
|
|
263
|
+
'.ios.ts',
|
|
264
|
+
'.ios.jsx',
|
|
265
|
+
'.ios.js',
|
|
266
|
+
'.native.tsx',
|
|
267
|
+
'.native.ts',
|
|
268
|
+
'.native.jsx',
|
|
269
|
+
'.native.js',
|
|
270
|
+
'.native.mjs',
|
|
271
|
+
]
|
|
272
|
+
const resolveNativePlatformFile = (source: string, importer: string): string | null => {
|
|
273
|
+
const base = path.resolve(path.dirname(importer), source)
|
|
274
|
+
for (const ext of nativeExts) {
|
|
275
|
+
const candidate = base + ext
|
|
276
|
+
try {
|
|
277
|
+
if (fs.existsSync(candidate)) return candidate
|
|
278
|
+
} catch {}
|
|
279
|
+
}
|
|
280
|
+
for (const ext of nativeExts) {
|
|
281
|
+
const candidate = path.join(base, 'index' + ext)
|
|
282
|
+
try {
|
|
283
|
+
if (fs.existsSync(candidate)) return candidate
|
|
284
|
+
} catch {}
|
|
285
|
+
}
|
|
286
|
+
return null
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
name: 'sootsim-keyboard-controller-bindings-redirect',
|
|
290
|
+
enforce: 'pre',
|
|
291
|
+
resolveId(source, importer) {
|
|
292
|
+
if (!importer) return null
|
|
293
|
+
if (!importer.includes('/react-native-keyboard-controller/')) return null
|
|
294
|
+
// keep bindings imports — strip optional .native and any leading
|
|
295
|
+
// `./`, `../`, `../../` so the depth doesn't matter.
|
|
296
|
+
const basename = source.split('/').pop() ?? ''
|
|
297
|
+
if (nativePlatformBasenames.has(basename)) {
|
|
298
|
+
return (
|
|
299
|
+
resolveNativePlatformFile(source, importer) ??
|
|
300
|
+
(basename === 'findNodeHandle' ? findNodeHandleTarget : null)
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
if (basename !== 'bindings' && basename !== 'bindings.native') return null
|
|
304
|
+
// don't redirect bindings imports that already resolved to our file
|
|
305
|
+
if (importer === target) return null
|
|
306
|
+
return target
|
|
307
|
+
},
|
|
308
|
+
load(id) {
|
|
309
|
+
if (id === findNodeHandleTarget) {
|
|
310
|
+
return `export { findNodeHandle } from ${JSON.stringify(rnShimPath)};`
|
|
311
|
+
}
|
|
312
|
+
return null
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// react-native-reanimated is mostly pure-JS — useSharedValue, useAnimatedStyle,
|
|
318
|
+
// useDerivedValue, withTiming/withSpring/etc., createAnimatedComponent, hooks,
|
|
319
|
+
// layout animations, easing all sit on top of a thin native seam:
|
|
320
|
+
//
|
|
321
|
+
// - src/specs/NativeReanimatedModule (turbomodule that installs __reanimatedModuleProxy)
|
|
322
|
+
// - src/platformFunctions/scrollTo
|
|
323
|
+
// - src/platformFunctions/measure
|
|
324
|
+
// - src/platformFunctions/setNativeProps
|
|
325
|
+
// - src/platformFunctions/dispatchCommand
|
|
326
|
+
// - src/platformFunctions/setGestureState
|
|
327
|
+
//
|
|
328
|
+
// per the project policy of not shimming pure-JS libs, we let upstream resolve
|
|
329
|
+
// from node_modules and redirect ONLY the native seam to our compat stub
|
|
330
|
+
// (one file at packages/compat/src/stubs/react-native-reanimated.ts that
|
|
331
|
+
// exports the right shape — default = turbomodule, named = platform fns).
|
|
332
|
+
//
|
|
333
|
+
// upstream's `assertWorkletsVersion` (DEV-only) imports a build-time script
|
|
334
|
+
// that does `require('react-native-worklets/package.json')`. our flat-file
|
|
335
|
+
// stub for react-native-worklets has no such subpath, so this throws under
|
|
336
|
+
// vite's worker build. redirect the validate script to a no-op stub.
|
|
337
|
+
const reanimatedNativeSeamFiles = new Set([
|
|
338
|
+
'NativeReanimatedModule',
|
|
339
|
+
'scrollTo',
|
|
340
|
+
'measure',
|
|
341
|
+
'setNativeProps',
|
|
342
|
+
'dispatchCommand',
|
|
343
|
+
'setGestureState',
|
|
344
|
+
])
|
|
345
|
+
const reanimatedFabricUtilsRedirectPath = path.resolve(
|
|
346
|
+
compatStubsDir,
|
|
347
|
+
'react-native-reanimated-fabric-utils.ts',
|
|
348
|
+
)
|
|
349
|
+
const reanimatedValidateWorkletsVersionPath = path.resolve(
|
|
350
|
+
compatStubsDir,
|
|
351
|
+
'react-native-reanimated-validate-worklets-version.ts',
|
|
352
|
+
)
|
|
353
|
+
export function reanimatedNativeSeamRedirect(): Plugin {
|
|
354
|
+
const target = path.resolve(compatStubsDir, 'react-native-reanimated.ts')
|
|
355
|
+
return {
|
|
356
|
+
name: 'sootsim-reanimated-native-seam-redirect',
|
|
357
|
+
enforce: 'pre',
|
|
358
|
+
resolveId(source, importer) {
|
|
359
|
+
// upstream's `assertWorkletsVersion` (DEV-only) imports a build-time
|
|
360
|
+
// script that does `require('react-native-worklets/package.json')`.
|
|
361
|
+
// our flat-file stub for react-native-worklets has no such subpath,
|
|
362
|
+
// so this throws under vite's worker build. resolve the package.json
|
|
363
|
+
// import to a synthetic virtual id and load synthetic JSON below.
|
|
364
|
+
if (source === 'react-native-worklets/package.json') {
|
|
365
|
+
return '\0sootsim:react-native-worklets-package-json'
|
|
366
|
+
}
|
|
367
|
+
if (!importer) return null
|
|
368
|
+
// no-op the assertWorkletsVersion validator — same reason. matches both
|
|
369
|
+
// the bare import and the .js-suffixed resolved form some bundlers see.
|
|
370
|
+
if (
|
|
371
|
+
source === 'react-native-reanimated/scripts/validate-worklets-version' ||
|
|
372
|
+
source === 'react-native-reanimated/scripts/validate-worklets-version.js'
|
|
373
|
+
) {
|
|
374
|
+
return reanimatedValidateWorkletsVersionPath
|
|
375
|
+
}
|
|
376
|
+
if (!importer.includes('/react-native-reanimated/')) return null
|
|
377
|
+
if (importer === target) return null
|
|
378
|
+
// strip optional `.web` / `.native` extension and leading `./`s — the
|
|
379
|
+
// redirect should match regardless of relative depth or platform suffix.
|
|
380
|
+
const basename = (source.split('/').pop() ?? '').replace(
|
|
381
|
+
/\.(native|web|ios|android)$/,
|
|
382
|
+
'',
|
|
383
|
+
)
|
|
384
|
+
// upstream's `fabricUtils.{ts,js}` `getShadowNodeWrapperFromRef` requires
|
|
385
|
+
// `ref.__internalInstanceHandle` (Fabric's react-shadow-tree handle) and
|
|
386
|
+
// throws otherwise. on sootsim refs come through as raw SootSimNodes
|
|
387
|
+
// without that handle. redirect to a sootsim-aware version that
|
|
388
|
+
// lazy-attaches the handle before reading it. see
|
|
389
|
+
// `react-native-reanimated-fabric-utils.ts`.
|
|
390
|
+
if (basename === 'fabricUtils' && importer !== reanimatedFabricUtilsRedirectPath) {
|
|
391
|
+
return reanimatedFabricUtilsRedirectPath
|
|
392
|
+
}
|
|
393
|
+
if (!reanimatedNativeSeamFiles.has(basename)) return null
|
|
394
|
+
if (process.env.SOOTSIM_REANIMATED_REDIRECT_DEBUG) {
|
|
395
|
+
console.log(
|
|
396
|
+
'[reanimatedRedirect]',
|
|
397
|
+
basename,
|
|
398
|
+
'<-',
|
|
399
|
+
source,
|
|
400
|
+
'from',
|
|
401
|
+
importer.slice(-80),
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
return target
|
|
405
|
+
},
|
|
406
|
+
load(id) {
|
|
407
|
+
// synthetic package.json for react-native-worklets — see resolveId
|
|
408
|
+
// above. just enough for upstream's version-validator to read `version`.
|
|
409
|
+
if (id === '\0sootsim:react-native-worklets-package-json') {
|
|
410
|
+
return `export default ${JSON.stringify({ version: '0.0.0-sootsim', name: 'react-native-worklets' })}`
|
|
411
|
+
}
|
|
412
|
+
return null
|
|
413
|
+
},
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// generalised native-seam redirect driven by NATIVE_SEAM_MANIFEST. each entry
|
|
418
|
+
// names an upstream package + a list of basenames to redirect to a single
|
|
419
|
+
// stub. agents migrating from wholesale-rewrite stubs to the native-seam
|
|
420
|
+
// pattern append entries here without touching the plugin code.
|
|
421
|
+
//
|
|
422
|
+
// the reanimated plugin above stays separate because it has package-specific
|
|
423
|
+
// edge cases (fabricUtils, validate-worklets-version). new migrations should
|
|
424
|
+
// use the manifest path unless they need similar special handling.
|
|
425
|
+
import { NATIVE_SEAM_MANIFEST } from './native-seam-manifest'
|
|
426
|
+
export function manifestNativeSeamRedirect(): Plugin {
|
|
427
|
+
// build a fast lookup: pkg → { basenames, target }
|
|
428
|
+
const byPkg = new Map<
|
|
429
|
+
string,
|
|
430
|
+
{ basenames: Set<string>; target: string; notes?: string }
|
|
431
|
+
>()
|
|
432
|
+
for (const entry of NATIVE_SEAM_MANIFEST) {
|
|
433
|
+
const existing = byPkg.get(entry.pkg)
|
|
434
|
+
if (existing) {
|
|
435
|
+
for (const b of entry.seamBasenames) existing.basenames.add(b)
|
|
436
|
+
} else {
|
|
437
|
+
byPkg.set(entry.pkg, {
|
|
438
|
+
basenames: new Set(entry.seamBasenames),
|
|
439
|
+
target: entry.target,
|
|
440
|
+
notes: entry.notes,
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
name: 'sootsim-manifest-native-seam-redirect',
|
|
446
|
+
enforce: 'pre',
|
|
447
|
+
resolveId(source, importer) {
|
|
448
|
+
if (!importer || byPkg.size === 0) return null
|
|
449
|
+
// find the package whose path appears in the importer. matches both
|
|
450
|
+
// `/<pkg>/` (unscoped) and `/@scope/<pkg>/` (scoped — the leading `@`
|
|
451
|
+
// is part of the pkg name in the manifest, e.g. `@notifee/react-native`).
|
|
452
|
+
let match: { basenames: Set<string>; target: string; notes?: string } | undefined
|
|
453
|
+
let matchedPkg: string | undefined
|
|
454
|
+
for (const [pkg, entry] of byPkg) {
|
|
455
|
+
if (importer.includes(`/${pkg}/`)) {
|
|
456
|
+
match = entry
|
|
457
|
+
matchedPkg = pkg
|
|
458
|
+
break
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (!match) return null
|
|
462
|
+
if (importer === match.target) return null
|
|
463
|
+
const basename = (source.split('/').pop() ?? '').replace(
|
|
464
|
+
/\.(native|web|ios|android)$/,
|
|
465
|
+
'',
|
|
466
|
+
)
|
|
467
|
+
if (!match.basenames.has(basename)) return null
|
|
468
|
+
if (process.env.SOOTSIM_NATIVE_SEAM_DEBUG) {
|
|
469
|
+
console.log(
|
|
470
|
+
'[nativeSeamRedirect]',
|
|
471
|
+
`${matchedPkg}:${basename}`,
|
|
472
|
+
'<-',
|
|
473
|
+
source,
|
|
474
|
+
'from',
|
|
475
|
+
importer.slice(-80),
|
|
476
|
+
match.notes ? `(${match.notes})` : '',
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
return match.target
|
|
480
|
+
},
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// start WS bridge server for debug CLI connectivity. exported so shell
|
|
485
|
+
// vite can install it standalone (engine builds via `vite build --watch`,
|
|
486
|
+
// which doesn't fire configureServer — the bridge has to run on shell).
|
|
487
|
+
export function wsBridgePlugin(): Plugin {
|
|
488
|
+
let bridgeHost: {
|
|
489
|
+
start?: (options?: { silent?: boolean }) => void | Promise<void>
|
|
490
|
+
close?: () => Promise<void> | void
|
|
491
|
+
} | null = null
|
|
492
|
+
return {
|
|
493
|
+
name: 'sootsim-ws-bridge',
|
|
494
|
+
configureServer(server) {
|
|
495
|
+
if (bridgeHost) return
|
|
496
|
+
// lazy import to avoid pulling ws into the main bundle.
|
|
497
|
+
// note: the .ts extension is required on vite 8 / node ESM — without
|
|
498
|
+
// it, import() throws "Cannot find module" and the bridge silently
|
|
499
|
+
// never starts.
|
|
500
|
+
void import('./host/bridge-host.ts')
|
|
501
|
+
.then(({ SootSimBridgeHost }) => {
|
|
502
|
+
bridgeHost = new SootSimBridgeHost({ port: DEFAULT_SOOTSIM_BRIDGE_PORT })
|
|
503
|
+
bridgeHost.start?.({ silent: true })
|
|
504
|
+
})
|
|
505
|
+
.catch((err: unknown) => {
|
|
506
|
+
// log but don't crash — some environments (e.g. production builds)
|
|
507
|
+
// don't have ws available. silently swallowing has caused debugging
|
|
508
|
+
// pain; print the message instead.
|
|
509
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
510
|
+
console.warn('[sootsim] ws bridge failed to start:', message)
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
// cleanup on server close (HMR restart)
|
|
514
|
+
server.httpServer?.on('close', () => {
|
|
515
|
+
if (bridgeHost) {
|
|
516
|
+
void bridgeHost.close?.()
|
|
517
|
+
bridgeHost = null
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
},
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// applies resolve, define, optimizeDeps config
|
|
525
|
+
function sootsimConfigPlugin(appDir: string, extraSources: string[]): Plugin {
|
|
526
|
+
return {
|
|
527
|
+
name: 'sootsim-config',
|
|
528
|
+
config(_, { mode }) {
|
|
529
|
+
const fsAllow = ['.']
|
|
530
|
+
for (const src of extraSources) fsAllow.push(src)
|
|
531
|
+
if (appDir) {
|
|
532
|
+
fsAllow.push(appDir)
|
|
533
|
+
const appNodeModules = path.join(appDir, 'node_modules')
|
|
534
|
+
if (fs.existsSync(appNodeModules)) fsAllow.push(appNodeModules)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// build alias list from builtin stubs
|
|
538
|
+
const stubAliases = Object.entries(builtinStubs)
|
|
539
|
+
// sort longest keys first so subpath aliases (e.g. foo/Bar) match before
|
|
540
|
+
// their parent package (e.g. foo) — vite processes aliases in order
|
|
541
|
+
.sort((a, b) => b[0].length - a[0].length)
|
|
542
|
+
.map(([find, file]) => ({
|
|
543
|
+
find,
|
|
544
|
+
replacement: path.resolve(compatStubsDir, file),
|
|
545
|
+
}))
|
|
546
|
+
|
|
547
|
+
const dedupe = ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime']
|
|
548
|
+
|
|
549
|
+
// internal sootsim builds must keep the entire Tamagui package family on a
|
|
550
|
+
// single module graph. mixing raw @fs modules with prebundled .vite/deps
|
|
551
|
+
// versions breaks context identity (Adapt/Portal/ZIndex/etc).
|
|
552
|
+
if (!appDir) {
|
|
553
|
+
dedupe.push(...internalTamaguiPackages)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const optimizeDepsExclude = [
|
|
557
|
+
'@tamagui/config',
|
|
558
|
+
'@tamagui/demos',
|
|
559
|
+
'react-native',
|
|
560
|
+
// tamagui native variants reference RN internals that the metro transform
|
|
561
|
+
// handles — must go through sootsim plugin, not rolldown pre-bundling
|
|
562
|
+
...(!appDir ? internalTamaguiPackages : []),
|
|
563
|
+
// ships .js with JSX that rolldown can't parse
|
|
564
|
+
'react-native-actions-sheet',
|
|
565
|
+
// these packages import `react-native-reanimated` and `react-native-gesture-handler`,
|
|
566
|
+
// both of which we alias to local stubs. pre-bundling would resolve those imports
|
|
567
|
+
// against the REAL packages (the alias only applies to main-plugin resolveId), so
|
|
568
|
+
// the resulting dep has a different reanimated instance than the app graph. keep
|
|
569
|
+
// them served live so our plugin aliases kick in.
|
|
570
|
+
'@gorhom/bottom-sheet',
|
|
571
|
+
'@gorhom/portal',
|
|
572
|
+
]
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
define: {
|
|
576
|
+
'process.env.NODE_ENV': JSON.stringify(mode),
|
|
577
|
+
'process.env.TEST_NATIVE_PLATFORM': JSON.stringify(''),
|
|
578
|
+
'process.env.TAMAGUI_TARGET': JSON.stringify('native'),
|
|
579
|
+
global: 'globalThis',
|
|
580
|
+
},
|
|
581
|
+
optimizeDeps: {
|
|
582
|
+
include: [
|
|
583
|
+
'react',
|
|
584
|
+
'react-dom',
|
|
585
|
+
'react-dom/client',
|
|
586
|
+
'react-reconciler',
|
|
587
|
+
'react-reconciler/constants',
|
|
588
|
+
'react/jsx-runtime',
|
|
589
|
+
'react/jsx-dev-runtime',
|
|
590
|
+
'@react-native/normalize-color',
|
|
591
|
+
// CJS deps transitively imported by @gorhom/bottom-sheet and
|
|
592
|
+
// @gorhom/portal (both excluded above). vite's CJS→ESM interop
|
|
593
|
+
// only kicks in via pre-bundle, so these must be pre-bundled even
|
|
594
|
+
// though their importers aren't.
|
|
595
|
+
'invariant',
|
|
596
|
+
'nanoid/non-secure',
|
|
597
|
+
],
|
|
598
|
+
exclude: optimizeDepsExclude,
|
|
599
|
+
// vite 8: rolldown replaces esbuild for dep optimization
|
|
600
|
+
rolldownOptions: {
|
|
601
|
+
resolve: {
|
|
602
|
+
conditionNames: ['react-native', 'import', 'require'],
|
|
603
|
+
mainFields: ['react-native', 'module', 'jsnext:main', 'jsnext'],
|
|
604
|
+
extensions: [
|
|
605
|
+
'.ios.tsx',
|
|
606
|
+
'.ios.ts',
|
|
607
|
+
'.ios.jsx',
|
|
608
|
+
'.ios.js',
|
|
609
|
+
'.native.tsx',
|
|
610
|
+
'.native.ts',
|
|
611
|
+
'.native.jsx',
|
|
612
|
+
'.native.js',
|
|
613
|
+
'.native.mjs',
|
|
614
|
+
'.mjs',
|
|
615
|
+
'.js',
|
|
616
|
+
'.mts',
|
|
617
|
+
'.ts',
|
|
618
|
+
'.jsx',
|
|
619
|
+
'.tsx',
|
|
620
|
+
'.json',
|
|
621
|
+
],
|
|
622
|
+
},
|
|
623
|
+
plugins: [
|
|
624
|
+
{
|
|
625
|
+
name: 'sootsim-stub-images',
|
|
626
|
+
resolveId(source: string, importer: string | undefined) {
|
|
627
|
+
if (
|
|
628
|
+
/\.(jpg|png|gif)$/.test(source) &&
|
|
629
|
+
importer?.includes('node_modules')
|
|
630
|
+
) {
|
|
631
|
+
return { id: '\0stub-image' }
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
load(id: string) {
|
|
635
|
+
if (id === '\0stub-image') return 'export default ""'
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
],
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
build: { target: 'esnext' },
|
|
642
|
+
oxc: { target: 'esnext' },
|
|
643
|
+
server: {
|
|
644
|
+
fs: { allow: fsAllow },
|
|
645
|
+
},
|
|
646
|
+
resolve: {
|
|
647
|
+
dedupe,
|
|
648
|
+
conditions: ['react-native', 'import', 'require'],
|
|
649
|
+
mainFields: ['react-native', 'module', 'jsnext:main', 'jsnext'],
|
|
650
|
+
extensions: [
|
|
651
|
+
'.ios.tsx',
|
|
652
|
+
'.ios.ts',
|
|
653
|
+
'.ios.jsx',
|
|
654
|
+
'.ios.js',
|
|
655
|
+
'.native.tsx',
|
|
656
|
+
'.native.ts',
|
|
657
|
+
'.native.jsx',
|
|
658
|
+
'.native.js',
|
|
659
|
+
'.native.mjs',
|
|
660
|
+
'.mjs',
|
|
661
|
+
'.js',
|
|
662
|
+
'.mts',
|
|
663
|
+
'.ts',
|
|
664
|
+
'.jsx',
|
|
665
|
+
'.tsx',
|
|
666
|
+
'.json',
|
|
667
|
+
],
|
|
668
|
+
alias: [
|
|
669
|
+
...sootsimBrowserSourceAliases,
|
|
670
|
+
...Object.entries(rnLibraryStubs).map(([find, replacement]) => ({
|
|
671
|
+
find,
|
|
672
|
+
replacement,
|
|
673
|
+
})),
|
|
674
|
+
{ find: /^react-native\/Libraries\/.*/, replacement: rnDeepStubDefault },
|
|
675
|
+
{ find: 'react-native', replacement: rnShimPath },
|
|
676
|
+
{ find: 'react-native-web', replacement: rnShimPath },
|
|
677
|
+
// resolved-path alias for the synthetic specifier the
|
|
678
|
+
// @react-navigation/native compat wrapper imports from. terminates
|
|
679
|
+
// the wrapper's own real-package import without re-entering the
|
|
680
|
+
// alias above. omit the entry if the package isn't installed —
|
|
681
|
+
// the wrapper file simply won't be loaded in that case.
|
|
682
|
+
...(reactNavigationNativeRealPath
|
|
683
|
+
? [
|
|
684
|
+
{
|
|
685
|
+
find: '@sootsim-internal/react-navigation-native-real',
|
|
686
|
+
replacement: reactNavigationNativeRealPath,
|
|
687
|
+
},
|
|
688
|
+
]
|
|
689
|
+
: []),
|
|
690
|
+
...(reactNativeHapticFeedbackTouchablePath
|
|
691
|
+
? [
|
|
692
|
+
{
|
|
693
|
+
find: SOOTSIM_HAPTIC_FEEDBACK_TOUCHABLE_SPECIFIER,
|
|
694
|
+
replacement: reactNativeHapticFeedbackTouchablePath,
|
|
695
|
+
},
|
|
696
|
+
]
|
|
697
|
+
: []),
|
|
698
|
+
...compatWrapperRealPackageAliases,
|
|
699
|
+
...stubAliases,
|
|
700
|
+
],
|
|
701
|
+
},
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// redirect `import ... from 'react'` to react-bridge for sootsim, compat, and
|
|
708
|
+
// react-reconciler code. this enables runtime React version switching for
|
|
709
|
+
// external app bundles — bindReact(appReact) swaps which React all code uses.
|
|
710
|
+
function reactBridgePlugin(): Plugin {
|
|
711
|
+
let isBuild = false
|
|
712
|
+
let redirectCount = 0
|
|
713
|
+
let skipCount = 0
|
|
714
|
+
return {
|
|
715
|
+
name: 'sootsim-react-bridge',
|
|
716
|
+
enforce: 'pre',
|
|
717
|
+
configResolved(config) {
|
|
718
|
+
isBuild = config.command === 'build'
|
|
719
|
+
console.log('[react-bridge-plugin] isBuild:', isBuild)
|
|
720
|
+
},
|
|
721
|
+
async resolveId(source, importer, options) {
|
|
722
|
+
if (source !== 'react' || !importer) return null
|
|
723
|
+
|
|
724
|
+
// log all react imports to see what's being processed
|
|
725
|
+
if (
|
|
726
|
+
importer.includes('/compat/') ||
|
|
727
|
+
importer.includes('/sootsim/src/') ||
|
|
728
|
+
importer.includes('/sootsim-engine/src/')
|
|
729
|
+
) {
|
|
730
|
+
skipCount++
|
|
731
|
+
if (skipCount <= 5) {
|
|
732
|
+
const skip = (options as { scan?: boolean })?.scan || options?.ssr || isBuild
|
|
733
|
+
if (!skip) {
|
|
734
|
+
console.log(
|
|
735
|
+
'[react-bridge-plugin] react import from:',
|
|
736
|
+
importer.split('/').slice(-3).join('/'),
|
|
737
|
+
'skip:',
|
|
738
|
+
skip,
|
|
739
|
+
)
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// skip during dep scanning / SSR — only active in browser serve mode
|
|
745
|
+
if ((options as { scan?: boolean })?.scan || options?.ssr || isBuild) return null
|
|
746
|
+
|
|
747
|
+
// only redirect imports from sootsim, sootsim-engine, compat, and react-reconciler
|
|
748
|
+
const shouldRedirect =
|
|
749
|
+
importer.includes('/sootsim/src/') ||
|
|
750
|
+
importer.includes('/sootsim-engine/src/') ||
|
|
751
|
+
importer.includes('/compat/src/') ||
|
|
752
|
+
importer.includes('react-reconciler')
|
|
753
|
+
|
|
754
|
+
if (!shouldRedirect) return null
|
|
755
|
+
|
|
756
|
+
// don't redirect react-bridge's own import of react (avoid infinite loop)
|
|
757
|
+
if (importer.includes('react-bridge')) return null
|
|
758
|
+
|
|
759
|
+
// stub-registry needs the real React (with __CLIENT_INTERNALS) for bundle stubs
|
|
760
|
+
if (importer.includes('stub-registry')) return null
|
|
761
|
+
|
|
762
|
+
redirectCount++
|
|
763
|
+
if (redirectCount <= 10) {
|
|
764
|
+
console.log(
|
|
765
|
+
'[react-bridge-plugin] REDIRECTING:',
|
|
766
|
+
importer.split('/').slice(-3).join('/'),
|
|
767
|
+
)
|
|
768
|
+
} else if (redirectCount === 11) {
|
|
769
|
+
console.log('[react-bridge-plugin] ... more redirects')
|
|
770
|
+
}
|
|
771
|
+
return { id: reactBridgePath, external: false }
|
|
772
|
+
},
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// worker-compatible react-bridge redirect. unlike the main plugin, this one
|
|
777
|
+
// also applies during builds since workers are built, not served. exported for
|
|
778
|
+
// use in worker.plugins configuration.
|
|
779
|
+
export function workerReactBridgePlugin(): Plugin {
|
|
780
|
+
return {
|
|
781
|
+
name: 'sootsim-worker-react-bridge',
|
|
782
|
+
enforce: 'pre',
|
|
783
|
+
async resolveId(source, importer, options) {
|
|
784
|
+
if (source !== 'react' || !importer) return null
|
|
785
|
+
|
|
786
|
+
// skip during dep scanning / SSR
|
|
787
|
+
if ((options as { scan?: boolean })?.scan || options?.ssr) return null
|
|
788
|
+
|
|
789
|
+
// don't redirect react-bridge's own import of react (avoid infinite loop)
|
|
790
|
+
if (importer.includes('react-bridge')) return null
|
|
791
|
+
|
|
792
|
+
// stub-registry needs the real React (with __CLIENT_INTERNALS) for bundle stubs
|
|
793
|
+
if (importer.includes('stub-registry')) return null
|
|
794
|
+
|
|
795
|
+
// redirect react imports from sootsim, sootsim-engine, compat, react-reconciler,
|
|
796
|
+
// and any pure-JS RN-ecosystem package the bundle pulls in. without this
|
|
797
|
+
// redirect, packages like react-native-drawer-layout / use-latest-callback
|
|
798
|
+
// call into a separate React instance than the renderer/bridge does, so
|
|
799
|
+
// their useRef/useEffect refs collide with the engine's hooks state and
|
|
800
|
+
// we hit "Maximum update depth exceeded" infinite-loop renders.
|
|
801
|
+
const shouldRedirect =
|
|
802
|
+
importer.includes('/sootsim/src/') ||
|
|
803
|
+
importer.includes('/sootsim-engine/src/') ||
|
|
804
|
+
importer.includes('/compat/src/') ||
|
|
805
|
+
importer.includes('react-reconciler') ||
|
|
806
|
+
importer.includes('/node_modules/')
|
|
807
|
+
|
|
808
|
+
if (!shouldRedirect) return null
|
|
809
|
+
|
|
810
|
+
return { id: reactBridgePath, external: false }
|
|
811
|
+
},
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// react 19's jsx-runtime.development.js wraps exports.jsx inside a conditional
|
|
816
|
+
// IIFE, so esbuild's static CJS analysis can't detect the named exports during
|
|
817
|
+
// pre-bundling. this plugin wraps the real pre-bundled module with explicit exports.
|
|
818
|
+
// exported for use in worker.plugins configuration.
|
|
819
|
+
export function fixJsxRuntimeExports(): Plugin {
|
|
820
|
+
return {
|
|
821
|
+
name: 'sootsim-fix-jsx-runtime',
|
|
822
|
+
enforce: 'pre',
|
|
823
|
+
async resolveId(source, importer, options) {
|
|
824
|
+
if (source === 'react/jsx-runtime' || source === 'react/jsx-dev-runtime') {
|
|
825
|
+
const resolved = await this.resolve(source, importer, {
|
|
826
|
+
...options,
|
|
827
|
+
skipSelf: true,
|
|
828
|
+
})
|
|
829
|
+
if (!resolved) return null
|
|
830
|
+
const prefix =
|
|
831
|
+
source === 'react/jsx-runtime'
|
|
832
|
+
? '\0sootsim:jsx-runtime:'
|
|
833
|
+
: '\0sootsim:jsx-dev-runtime:'
|
|
834
|
+
return prefix + resolved.id
|
|
835
|
+
}
|
|
836
|
+
return null
|
|
837
|
+
},
|
|
838
|
+
load(id) {
|
|
839
|
+
if (id.startsWith('\0sootsim:jsx-runtime:')) {
|
|
840
|
+
const realId = id.slice('\0sootsim:jsx-runtime:'.length)
|
|
841
|
+
return [
|
|
842
|
+
`import * as _mod from ${JSON.stringify(realId)};`,
|
|
843
|
+
`const _m = _mod.default || _mod;`,
|
|
844
|
+
`export const jsx = _m.jsx || _mod.jsx;`,
|
|
845
|
+
`export const jsxs = _m.jsxs || _mod.jsxs;`,
|
|
846
|
+
`export const Fragment = _m.Fragment || _mod.Fragment;`,
|
|
847
|
+
].join('\n')
|
|
848
|
+
}
|
|
849
|
+
if (id.startsWith('\0sootsim:jsx-dev-runtime:')) {
|
|
850
|
+
const realId = id.slice('\0sootsim:jsx-dev-runtime:'.length)
|
|
851
|
+
// react/jsx-runtime is already wrapped by this plugin (above) to expose
|
|
852
|
+
// { jsx, jsxs, Fragment } as named exports, so read them directly from
|
|
853
|
+
// the namespace — no `_runtime.default || _runtime` fallback needed.
|
|
854
|
+
// rolldown statically knows the wrapped module has no default export
|
|
855
|
+
// and would emit IMPORT_IS_UNDEFINED warnings on every build.
|
|
856
|
+
return [
|
|
857
|
+
`import * as _mod from ${JSON.stringify(realId)};`,
|
|
858
|
+
`import * as _runtime from "react/jsx-runtime";`,
|
|
859
|
+
`const _m = _mod.default || _mod;`,
|
|
860
|
+
// production React builds may not expose jsxDEV from react/jsx-dev-runtime.
|
|
861
|
+
// fall back to jsx so transformed modules still execute.
|
|
862
|
+
`export const jsxDEV = _m.jsxDEV || _mod.jsxDEV || _m.jsx || _mod.jsx || _runtime.jsx;`,
|
|
863
|
+
`export const Fragment = _m.Fragment || _mod.Fragment || _runtime.Fragment;`,
|
|
864
|
+
].join('\n')
|
|
865
|
+
}
|
|
866
|
+
return null
|
|
867
|
+
},
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// esbuild plugin for optimizeDeps pre-bundling
|
|
872
|
+
function sootsimEsbuildPlugin(appDir: string) {
|
|
873
|
+
const platformExts = [
|
|
874
|
+
'.ios.tsx',
|
|
875
|
+
'.ios.ts',
|
|
876
|
+
'.ios.jsx',
|
|
877
|
+
'.ios.js',
|
|
878
|
+
'.native.tsx',
|
|
879
|
+
'.native.ts',
|
|
880
|
+
'.native.jsx',
|
|
881
|
+
'.native.js',
|
|
882
|
+
'.native.mjs',
|
|
883
|
+
]
|
|
884
|
+
|
|
885
|
+
function isAppSourceEsbuild(filePath: string): boolean {
|
|
886
|
+
return !!(appDir && filePath.startsWith(appDir))
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return {
|
|
890
|
+
name: 'sootsim-esbuild',
|
|
891
|
+
setup(build: any) {
|
|
892
|
+
// metro-style .ios > .native resolution
|
|
893
|
+
build.onResolve({ filter: /^\./ }, (args: any) => {
|
|
894
|
+
if (!args.importer.includes('node_modules') && !isAppSourceEsbuild(args.importer))
|
|
895
|
+
return null
|
|
896
|
+
const dir = path.dirname(args.importer)
|
|
897
|
+
const resolved = path.resolve(dir, args.path)
|
|
898
|
+
for (const ext of platformExts) {
|
|
899
|
+
const candidate = resolved + ext
|
|
900
|
+
try {
|
|
901
|
+
if (fs.existsSync(candidate)) return { path: candidate }
|
|
902
|
+
} catch {}
|
|
903
|
+
}
|
|
904
|
+
for (const ext of platformExts) {
|
|
905
|
+
const candidate = path.join(resolved, 'index' + ext)
|
|
906
|
+
try {
|
|
907
|
+
if (fs.existsSync(candidate)) return { path: candidate }
|
|
908
|
+
} catch {}
|
|
909
|
+
}
|
|
910
|
+
const allExts = [...platformExts, '.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs']
|
|
911
|
+
for (const ext of allExts) {
|
|
912
|
+
if (args.path.endsWith(ext)) {
|
|
913
|
+
const base = resolved.slice(0, -ext.length)
|
|
914
|
+
for (const pext of platformExts) {
|
|
915
|
+
const candidate = base + pext
|
|
916
|
+
try {
|
|
917
|
+
if (fs.existsSync(candidate)) return { path: candidate }
|
|
918
|
+
} catch {}
|
|
919
|
+
}
|
|
920
|
+
break
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return null
|
|
924
|
+
})
|
|
925
|
+
|
|
926
|
+
// react-native deep path stubs
|
|
927
|
+
for (const [importPath, stubPath] of Object.entries(rnLibraryStubs)) {
|
|
928
|
+
build.onResolve(
|
|
929
|
+
{ filter: new RegExp(`^${importPath.replace(/\//g, '\\/')}$`) },
|
|
930
|
+
() => ({
|
|
931
|
+
path: stubPath,
|
|
932
|
+
}),
|
|
933
|
+
)
|
|
934
|
+
}
|
|
935
|
+
build.onResolve({ filter: /^react-native\/Libraries\// }, (args: any) => {
|
|
936
|
+
return { path: rnLibraryStubs[args.path] || rnDeepStubDefault }
|
|
937
|
+
})
|
|
938
|
+
|
|
939
|
+
// resolve builtin stubs (react-native-safe-area-context, expo-*, etc.)
|
|
940
|
+
for (const [pkgName, stubFile] of Object.entries(builtinStubs)) {
|
|
941
|
+
const stubPath = path.resolve(compatStubsDir, stubFile)
|
|
942
|
+
const escaped = pkgName.replace(/[.*+?^${}()|[\]\\/@-]/g, '\\$&')
|
|
943
|
+
build.onResolve({ filter: new RegExp(`^${escaped}$`) }, () => ({
|
|
944
|
+
path: stubPath,
|
|
945
|
+
}))
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// external app node_modules resolution — use esbuild's own resolve
|
|
949
|
+
// so exports conditions are respected (not CJS require.resolve which ignores them)
|
|
950
|
+
if (appDir) {
|
|
951
|
+
const owned = new Set(coreOwnedPackages)
|
|
952
|
+
build.onResolve({ filter: /^[^.]/ }, async (args: any) => {
|
|
953
|
+
if (args.pluginData?.fromAppResolve) return null
|
|
954
|
+
if (
|
|
955
|
+
!isAppSourceEsbuild(args.importer) &&
|
|
956
|
+
!args.importer.includes(path.join(appDir, 'node_modules'))
|
|
957
|
+
)
|
|
958
|
+
return null
|
|
959
|
+
const src = args.path
|
|
960
|
+
if (src.startsWith('\0')) return null
|
|
961
|
+
if (owned.has(src) || owned.has(getPackageName(src))) return null
|
|
962
|
+
if (src === 'react-native' || src.startsWith('react-native/')) return null
|
|
963
|
+
try {
|
|
964
|
+
const result = await build.resolve(src, {
|
|
965
|
+
resolveDir: appDir,
|
|
966
|
+
pluginData: { fromAppResolve: true },
|
|
967
|
+
})
|
|
968
|
+
if (result.errors.length) return null
|
|
969
|
+
return { path: result.path }
|
|
970
|
+
} catch {
|
|
971
|
+
return null
|
|
972
|
+
}
|
|
973
|
+
})
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// auto-stub missing native packages
|
|
977
|
+
build.onResolve(
|
|
978
|
+
{
|
|
979
|
+
filter:
|
|
980
|
+
/^(expo-|react-native-|@react-native\/|@react-native-community\/|@expo\/|expo$)/,
|
|
981
|
+
},
|
|
982
|
+
(args: any) => {
|
|
983
|
+
const pkgName = getPackageName(args.path)
|
|
984
|
+
if (moduleExistsIn(pkgName, sootsimRoot)) return null
|
|
985
|
+
if (appDir && moduleExistsIn(pkgName, appDir)) return null
|
|
986
|
+
return { path: nativeAutoStubPath }
|
|
987
|
+
},
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
// stub missing image imports
|
|
991
|
+
build.onResolve({ filter: /\.(jpg|png|gif)$/ }, (args: any) => {
|
|
992
|
+
if (args.importer.includes('node_modules')) {
|
|
993
|
+
return { path: args.path, namespace: 'stub-image' }
|
|
994
|
+
}
|
|
995
|
+
return null
|
|
996
|
+
})
|
|
997
|
+
build.onLoad({ filter: /.*/, namespace: 'stub-image' }, () => ({
|
|
998
|
+
contents: 'export default ""',
|
|
999
|
+
loader: 'js',
|
|
1000
|
+
}))
|
|
1001
|
+
},
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// handle .js files containing JSX from node_modules during production builds.
|
|
1006
|
+
// upstream `react-native-reanimated` and downstream consumers (
|
|
1007
|
+
// `react-native-keyboard-controller`, `react-native-gesture-handler`,
|
|
1008
|
+
// `react-native-screens`, sootsim's own fixtures + RN compat source) ship
|
|
1009
|
+
// raw source containing `'worklet'` directives and inline updaters to
|
|
1010
|
+
// auto-workletizable hooks. the `react-native-worklets/plugin` babel
|
|
1011
|
+
// transform lifts those into wrapper functions carrying `__closure`,
|
|
1012
|
+
// `__workletHash`, and `__initData`. without that transform,
|
|
1013
|
+
// `useAnimatedStyle`'s `let inputs = Object.values(updater.__closure ?? {})`
|
|
1014
|
+
// returns `[]` and the mapper never subscribes — animations don't run.
|
|
1015
|
+
//
|
|
1016
|
+
// metro builds (apps loaded inside sootsim — 3pc, bluesky, expensify,
|
|
1017
|
+
// eigen, …) run the plugin in their own pipeline, so the bundle that
|
|
1018
|
+
// reaches us already has `__closure` baked in. our transform only needs
|
|
1019
|
+
// to act on:
|
|
1020
|
+
//
|
|
1021
|
+
// - sootsim's own src + test fixtures + external-app source, and
|
|
1022
|
+
// - upstream library source loaded from node_modules where the redirect
|
|
1023
|
+
// plugin let pure-JS resolve naturally.
|
|
1024
|
+
//
|
|
1025
|
+
// the keyword list + ignored-path list is ported from
|
|
1026
|
+
// `~/one/packages/compiler/src/transformBabel.ts` (which is itself ported
|
|
1027
|
+
// from reanimated 3.15.1's autoworkletization keywords). keep the two in
|
|
1028
|
+
// sync if upstream adds new auto-workletized hooks. detection is
|
|
1029
|
+
// content-driven, not path-driven, so we don't need a per-package
|
|
1030
|
+
// allow-list — anything that mentions a workletizable identifier and
|
|
1031
|
+
// isn't on the ignored-path list goes through.
|
|
1032
|
+
|
|
1033
|
+
export function workletsBabelTransform(
|
|
1034
|
+
// accepted for future per-target overrides; matching is content-driven
|
|
1035
|
+
// today so we don't actually consult it. kept for plugin-list parity.
|
|
1036
|
+
_isAppSource: (p: string) => boolean = () => false,
|
|
1037
|
+
): Plugin {
|
|
1038
|
+
return {
|
|
1039
|
+
name: 'sootsim-worklets-babel-transform',
|
|
1040
|
+
enforce: 'pre',
|
|
1041
|
+
async transform(code, id) {
|
|
1042
|
+
if (!shouldApplyWorkletsPlugin(id, code)) return null
|
|
1043
|
+
try {
|
|
1044
|
+
if (process.env.SOOTSIM_WORKLETS_BABEL_DEBUG) {
|
|
1045
|
+
console.log('[worklets-babel] transforming', id.slice(-80))
|
|
1046
|
+
}
|
|
1047
|
+
return await transformWorkletsCode(code, id, true)
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
// surface but don't kill the build — fall back to raw code so the
|
|
1050
|
+
// rest of the pipeline can run. these failures are loud in the
|
|
1051
|
+
// console and easy to chase.
|
|
1052
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
1053
|
+
console.warn(`[sootsim-worklets-babel] transform failed for ${id}: ${message}`)
|
|
1054
|
+
return null
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// rolldown can't parse JSX in .js files — transform them first.
|
|
1061
|
+
// needed because some RN packages (e.g. react-native-actions-sheet) ship JSX in .js.
|
|
1062
|
+
// some upstream RN libraries (e.g. @react-native-segmented-control/segmented-control)
|
|
1063
|
+
// publish raw Flow source as their runtime entry. Metro strips Flow in its babel
|
|
1064
|
+
// pipeline; vite/rolldown/oxc do not. detect the `@flow` pragma in node_modules and
|
|
1065
|
+
// run @babel/plugin-transform-flow-strip-types so the wrapper-real seam can pull
|
|
1066
|
+
// in the real upstream JS without re-implementing it locally. mirrored in
|
|
1067
|
+
// scripts/build-bundler-deps.ts so esbuild-based dep prebundles stay in sync.
|
|
1068
|
+
let flowBabel: typeof import('@babel/core') | null = null
|
|
1069
|
+
let flowStripPlugin: import('@babel/core').PluginItem | null = null
|
|
1070
|
+
let jsxSyntaxPlugin: import('@babel/core').PluginItem | null = null
|
|
1071
|
+
function loadFlowBabel(): void {
|
|
1072
|
+
if (flowBabel) return
|
|
1073
|
+
flowBabel = sootsimPluginRequire('@babel/core')
|
|
1074
|
+
flowStripPlugin = sootsimPluginRequire(
|
|
1075
|
+
'@babel/plugin-transform-flow-strip-types',
|
|
1076
|
+
) as import('@babel/core').PluginItem
|
|
1077
|
+
jsxSyntaxPlugin = sootsimPluginRequire(
|
|
1078
|
+
'@babel/plugin-syntax-jsx',
|
|
1079
|
+
) as import('@babel/core').PluginItem
|
|
1080
|
+
}
|
|
1081
|
+
function flowStripTransform(): Plugin {
|
|
1082
|
+
return {
|
|
1083
|
+
name: 'sootsim-flow-strip',
|
|
1084
|
+
enforce: 'pre',
|
|
1085
|
+
transform(code, id) {
|
|
1086
|
+
if (!id.includes('/node_modules/')) return null
|
|
1087
|
+
if (!/\.[cm]?jsx?$/.test(id)) return null
|
|
1088
|
+
if (!code.includes('@flow')) return null
|
|
1089
|
+
loadFlowBabel()
|
|
1090
|
+
const result = flowBabel!.transformSync(code, {
|
|
1091
|
+
filename: id,
|
|
1092
|
+
babelrc: false,
|
|
1093
|
+
configFile: false,
|
|
1094
|
+
plugins: [flowStripPlugin!, jsxSyntaxPlugin!],
|
|
1095
|
+
sourceMaps: true,
|
|
1096
|
+
compact: false,
|
|
1097
|
+
})
|
|
1098
|
+
if (!result?.code) return null
|
|
1099
|
+
return { code: result.code, map: result.map ?? null }
|
|
1100
|
+
},
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function nodeModulesJsxTransform(): Plugin {
|
|
1105
|
+
return {
|
|
1106
|
+
name: 'sootsim-node-modules-jsx',
|
|
1107
|
+
enforce: 'pre',
|
|
1108
|
+
async transform(code, id) {
|
|
1109
|
+
if (!id.includes('/node_modules/')) return null
|
|
1110
|
+
if (!id.endsWith('.js')) return null
|
|
1111
|
+
if (!code.includes('<')) return null
|
|
1112
|
+
// quick check: does it look like JSX? (angle bracket followed by uppercase or known RN component)
|
|
1113
|
+
if (!/[=(\s,]<[A-Z]/.test(code) && !/<\/[A-Z]/.test(code)) return null
|
|
1114
|
+
const needsReact =
|
|
1115
|
+
!code.includes('import React') && !code.includes('import * as React')
|
|
1116
|
+
const withReact = needsReact ? `import React from 'react';\n${code}` : code
|
|
1117
|
+
const transformed = await transformWithOxc(withReact, id, {
|
|
1118
|
+
lang: 'jsx',
|
|
1119
|
+
jsx: { runtime: 'classic' },
|
|
1120
|
+
})
|
|
1121
|
+
return { code: transformed.code, map: transformed.map }
|
|
1122
|
+
},
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// metro-style platform resolution: .ios > .native > base
|
|
1127
|
+
function metroNativeResolve(isAppSource: (p: string) => boolean): Plugin {
|
|
1128
|
+
const platformExts = [
|
|
1129
|
+
'.ios.tsx',
|
|
1130
|
+
'.ios.ts',
|
|
1131
|
+
'.ios.jsx',
|
|
1132
|
+
'.ios.js',
|
|
1133
|
+
'.native.tsx',
|
|
1134
|
+
'.native.ts',
|
|
1135
|
+
'.native.jsx',
|
|
1136
|
+
'.native.js',
|
|
1137
|
+
'.native.mjs',
|
|
1138
|
+
]
|
|
1139
|
+
const allExts = [...platformExts, '.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs']
|
|
1140
|
+
|
|
1141
|
+
function tryResolve(base: string): string | null {
|
|
1142
|
+
for (const ext of platformExts) {
|
|
1143
|
+
const candidate = base + ext
|
|
1144
|
+
try {
|
|
1145
|
+
if (fs.existsSync(candidate)) return candidate
|
|
1146
|
+
} catch {}
|
|
1147
|
+
}
|
|
1148
|
+
for (const ext of platformExts) {
|
|
1149
|
+
const candidate = path.join(base, 'index' + ext)
|
|
1150
|
+
try {
|
|
1151
|
+
if (fs.existsSync(candidate)) return candidate
|
|
1152
|
+
} catch {}
|
|
1153
|
+
}
|
|
1154
|
+
return null
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return {
|
|
1158
|
+
name: 'sootsim-metro-native-resolve',
|
|
1159
|
+
enforce: 'pre',
|
|
1160
|
+
resolveId(source, importer) {
|
|
1161
|
+
if (!importer) return null
|
|
1162
|
+
if (!importer.includes('node_modules') && !isAppSource(importer)) return null
|
|
1163
|
+
if (!source.startsWith('.')) return null
|
|
1164
|
+
if (platformExts.some((ext) => source.endsWith(ext))) return null
|
|
1165
|
+
|
|
1166
|
+
const hasBaseExt = allExts.some((ext) => source.endsWith(ext))
|
|
1167
|
+
const dir = path.dirname(importer)
|
|
1168
|
+
|
|
1169
|
+
if (hasBaseExt) {
|
|
1170
|
+
const resolved = path.resolve(dir, source)
|
|
1171
|
+
for (const ext of allExts) {
|
|
1172
|
+
if (source.endsWith(ext)) {
|
|
1173
|
+
const found = tryResolve(resolved.slice(0, -ext.length))
|
|
1174
|
+
if (found) return found
|
|
1175
|
+
break
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
} else {
|
|
1179
|
+
const found = tryResolve(path.resolve(dir, source))
|
|
1180
|
+
if (found) return found
|
|
1181
|
+
}
|
|
1182
|
+
return null
|
|
1183
|
+
},
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// convert require("react-native") and deep library requires to ESM imports
|
|
1188
|
+
function reactNativeRequirePlugin(isAppSource: (p: string) => boolean): Plugin {
|
|
1189
|
+
return {
|
|
1190
|
+
name: 'sootsim-react-native-require',
|
|
1191
|
+
enforce: 'pre',
|
|
1192
|
+
transform(code, id) {
|
|
1193
|
+
if (!id.includes('node_modules') && !isAppSource(id)) return null
|
|
1194
|
+
if (!code.includes('react-native')) return null
|
|
1195
|
+
|
|
1196
|
+
let hasChanges = false
|
|
1197
|
+
let imports = ''
|
|
1198
|
+
let result = code
|
|
1199
|
+
let importCounter = 0
|
|
1200
|
+
|
|
1201
|
+
if (
|
|
1202
|
+
result.includes('require("react-native")') ||
|
|
1203
|
+
result.includes("require('react-native')")
|
|
1204
|
+
) {
|
|
1205
|
+
imports += `import * as __soot_rn_shim__ from ${JSON.stringify(rnShimPath)};\n`
|
|
1206
|
+
result = result.replace(/require\(["']react-native["']\)/g, '__soot_rn_shim__')
|
|
1207
|
+
hasChanges = true
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const deepRequireRegex = /require\(["'](react-native\/Libraries\/[^"']+)["']\)/g
|
|
1211
|
+
const replacements: Array<{ full: string; path: string; varName: string }> = []
|
|
1212
|
+
const seenPaths = new Map<string, string>()
|
|
1213
|
+
let match
|
|
1214
|
+
|
|
1215
|
+
deepRequireRegex.lastIndex = 0
|
|
1216
|
+
while ((match = deepRequireRegex.exec(result)) !== null) {
|
|
1217
|
+
const importPath = match[1]
|
|
1218
|
+
if (!seenPaths.has(importPath)) {
|
|
1219
|
+
const varName = `__soot_rn_lib_${importCounter++}__`
|
|
1220
|
+
const stubPath = rnLibraryStubs[importPath] || rnDeepStubDefault
|
|
1221
|
+
imports += `import * as ${varName} from ${JSON.stringify(stubPath)};\n`
|
|
1222
|
+
seenPaths.set(importPath, varName)
|
|
1223
|
+
}
|
|
1224
|
+
replacements.push({
|
|
1225
|
+
full: match[0],
|
|
1226
|
+
path: importPath,
|
|
1227
|
+
varName: seenPaths.get(importPath)!,
|
|
1228
|
+
})
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
if (replacements.length > 0) {
|
|
1232
|
+
for (const rep of replacements) {
|
|
1233
|
+
result = result.replace(rep.full, rep.varName)
|
|
1234
|
+
}
|
|
1235
|
+
hasChanges = true
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
if (!hasChanges) return null
|
|
1239
|
+
return { code: imports + result, map: null }
|
|
1240
|
+
},
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// resolve bare imports from external app's node_modules
|
|
1245
|
+
function externalAppResolvePlugin(
|
|
1246
|
+
appDir: string,
|
|
1247
|
+
isAppSource: (p: string) => boolean,
|
|
1248
|
+
ownedPackages: Set<string>,
|
|
1249
|
+
): Plugin {
|
|
1250
|
+
if (!appDir) return { name: 'sootsim-external-app-resolve' }
|
|
1251
|
+
|
|
1252
|
+
// synthetic importer inside the app dir so vite searches app's node_modules
|
|
1253
|
+
// using its conditions-aware exports resolution (not CJS require.resolve)
|
|
1254
|
+
const appVirtualImporter = path.join(appDir, '_virtual_.js')
|
|
1255
|
+
|
|
1256
|
+
return {
|
|
1257
|
+
name: 'sootsim-external-app-resolve',
|
|
1258
|
+
enforce: 'pre',
|
|
1259
|
+
async resolveId(source, importer, options) {
|
|
1260
|
+
if (!importer) return null
|
|
1261
|
+
if (source.startsWith('.') || source.startsWith('/') || source.startsWith('\0'))
|
|
1262
|
+
return null
|
|
1263
|
+
if (ownedPackages.has(source) || ownedPackages.has(getPackageName(source)))
|
|
1264
|
+
return null
|
|
1265
|
+
if (!isAppSource(importer) && !importer.includes(path.join(appDir, 'node_modules')))
|
|
1266
|
+
return null
|
|
1267
|
+
if (source === 'react-native' || source.startsWith('react-native/')) return null
|
|
1268
|
+
|
|
1269
|
+
// re-resolve through vite's pipeline so exports conditions (react-native, import) are respected
|
|
1270
|
+
const resolved = await this.resolve(source, appVirtualImporter, {
|
|
1271
|
+
...options,
|
|
1272
|
+
skipSelf: true,
|
|
1273
|
+
})
|
|
1274
|
+
return resolved || null
|
|
1275
|
+
},
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// convert require() calls and handle JSX in .js files from external app sources
|
|
1280
|
+
function externalAppTransformPlugin(isAppSource: (p: string) => boolean): Plugin {
|
|
1281
|
+
return {
|
|
1282
|
+
name: 'sootsim-external-app-transform',
|
|
1283
|
+
enforce: 'pre',
|
|
1284
|
+
async transform(code, id) {
|
|
1285
|
+
if (!isAppSource(id)) return null
|
|
1286
|
+
|
|
1287
|
+
let result = code
|
|
1288
|
+
let imports = ''
|
|
1289
|
+
let importCounter = 0
|
|
1290
|
+
let hasChanges = false
|
|
1291
|
+
|
|
1292
|
+
if (code.includes('require(')) {
|
|
1293
|
+
const requireRegex = /require\(["']([^"']+)["']\)/g
|
|
1294
|
+
const replacements: Array<{ full: string; varName: string }> = []
|
|
1295
|
+
const seenPaths = new Map<string, string>()
|
|
1296
|
+
let match
|
|
1297
|
+
|
|
1298
|
+
requireRegex.lastIndex = 0
|
|
1299
|
+
while ((match = requireRegex.exec(result)) !== null) {
|
|
1300
|
+
const reqPath = match[1]
|
|
1301
|
+
if (reqPath === 'react-native' || reqPath.startsWith('react-native/')) continue
|
|
1302
|
+
|
|
1303
|
+
if (!seenPaths.has(reqPath)) {
|
|
1304
|
+
const varName = `__soot_cjs_${importCounter++}__`
|
|
1305
|
+
imports += `import * as ${varName} from ${JSON.stringify(reqPath)};\n`
|
|
1306
|
+
seenPaths.set(reqPath, varName)
|
|
1307
|
+
}
|
|
1308
|
+
replacements.push({ full: match[0], varName: seenPaths.get(reqPath)! })
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if (replacements.length > 0) {
|
|
1312
|
+
for (const rep of replacements) {
|
|
1313
|
+
result = result.replace(rep.full, rep.varName)
|
|
1314
|
+
}
|
|
1315
|
+
hasChanges = true
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
if (imports) result = imports + result
|
|
1320
|
+
|
|
1321
|
+
if (id.endsWith('.js') && result.includes('<')) {
|
|
1322
|
+
const needsReact =
|
|
1323
|
+
!result.includes('import React') && !result.includes('import * as React')
|
|
1324
|
+
const withReact = needsReact ? `import React from 'react';\n${result}` : result
|
|
1325
|
+
const transformed = await transformWithOxc(withReact, id, {
|
|
1326
|
+
lang: 'jsx',
|
|
1327
|
+
jsx: { runtime: 'classic' },
|
|
1328
|
+
})
|
|
1329
|
+
return { code: transformed.code, map: transformed.map }
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
if (!hasChanges) return null
|
|
1333
|
+
return { code: result, map: null }
|
|
1334
|
+
},
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// auto-stub native packages that don't exist in any node_modules
|
|
1339
|
+
function stubMissingNativeDeps(appDir: string): Plugin {
|
|
1340
|
+
const checked = new Map<string, boolean>()
|
|
1341
|
+
|
|
1342
|
+
function packageExists(pkgName: string): boolean {
|
|
1343
|
+
const cached = checked.get(pkgName)
|
|
1344
|
+
if (cached !== undefined) return cached
|
|
1345
|
+
let exists = moduleExistsIn(pkgName, sootsimRoot)
|
|
1346
|
+
if (!exists && appDir) exists = moduleExistsIn(pkgName, appDir)
|
|
1347
|
+
checked.set(pkgName, exists)
|
|
1348
|
+
return exists
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
return {
|
|
1352
|
+
name: 'sootsim-stub-missing-native-deps',
|
|
1353
|
+
resolveId(source) {
|
|
1354
|
+
if (!isNativePackage(source)) return null
|
|
1355
|
+
const pkgName = getPackageName(source)
|
|
1356
|
+
if (packageExists(pkgName)) return null
|
|
1357
|
+
console.log(`[sootsim] auto-stubbing missing native dep: ${source}`)
|
|
1358
|
+
return nativeAutoStubPath
|
|
1359
|
+
},
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// stub missing image imports from node_modules
|
|
1364
|
+
function stubMissingImages(isAppSource: (p: string) => boolean): Plugin {
|
|
1365
|
+
return {
|
|
1366
|
+
name: 'sootsim-stub-images',
|
|
1367
|
+
enforce: 'pre',
|
|
1368
|
+
resolveId(id, importer) {
|
|
1369
|
+
if (id.endsWith('.jpg') || id.endsWith('.png') || id.endsWith('.gif')) {
|
|
1370
|
+
if (importer?.includes('node_modules') || (importer && isAppSource(importer))) {
|
|
1371
|
+
return '\0stub-image'
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return null
|
|
1375
|
+
},
|
|
1376
|
+
load(id) {
|
|
1377
|
+
if (id === '\0stub-image') return 'export default ""'
|
|
1378
|
+
return null
|
|
1379
|
+
},
|
|
1380
|
+
}
|
|
1381
|
+
}
|