peachsim 0.1.626 → 0.1.628
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 +1 -1
- package/README.md +3 -2
- package/cli/bin.ts +6 -2
- package/cli/bridge-flow-runner.ts +79 -72
- package/cli/commands/box/bundle-plane.ts +7 -2
- package/cli/commands/box.ts +9 -2
- package/cli/commands/cleanup.ts +6 -6
- package/cli/commands/control.ts +36 -6
- package/cli/commands/detox.ts +25 -11
- package/cli/commands/flow.ts +20 -8
- package/cli/commands/inspect.ts +22 -37
- package/cli/commands/maestro-generate.ts +5 -12
- package/cli/commands/maestro.ts +16 -6
- package/cli/commands/mode.ts +11 -7
- package/cli/commands/platform.ts +1 -0
- package/cli/commands/report-issue.ts +1 -1
- package/cli/commands/setup.ts +16 -11
- package/cli/commands/show-touches-mode.ts +33 -0
- package/cli/commands/skills.ts +16 -0
- package/cli/commands/test.ts +22 -1
- package/cli/commands/upload.ts +18 -8
- package/cli/commands/welcome.ts +3 -3
- package/cli/drivers/playwright-sim-host.ts +16 -8
- package/cli/drivers/playwright.ts +8 -5
- package/cli/help-core.ts +9 -4
- package/cli/internal-child.ts +14 -2
- package/cli/levenshtein.ts +27 -0
- package/cli/main.ts +28 -18
- package/cli/run-rnx.ts +8 -4
- package/cli/ws-bridge.ts +3 -2
- package/detox/index.ts +234 -7
- package/dist-lib/agent-daemon-client.cjs +2 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.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 +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +4 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +18 -8
- package/dist-lib/capture-contract.mjs +18 -8
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +170 -13
- package/dist-lib/dev-bundle-resolution.cjs +2 -2
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +280 -138
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +62 -12
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +7 -1
- package/dist-lib/menu.mjs +7 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +43 -4
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +215 -28
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +431 -276
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +14512 -14208
- package/dist-lib/vite-kotlin.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +44 -4
- package/install.mjs +16 -0
- package/install.sh +199 -0
- package/package.json +6 -1
- package/scripts/dev-server-scanner.ts +77 -46
- package/skills/contrast/SKILL.md +1 -1
- package/src/bridge-constants.ts +11 -0
- package/src/capture-contract.ts +40 -7
- package/src/dev-bundle-resolution.ts +1 -1
- package/src/host/bridge-host.ts +26 -137
- package/src/menu.ts +7 -0
- package/src/native-seam-manifest.ts +4 -16
- package/src/runtime-assets.ts +59 -2
- package/src/vite-plugin.ts +723 -25
package/src/vite-plugin.ts
CHANGED
|
@@ -8,11 +8,20 @@
|
|
|
8
8
|
// // with external app:
|
|
9
9
|
// export default defineConfig({ plugins: [sootsim({ app: '~/my-rn-app' })] })
|
|
10
10
|
|
|
11
|
+
import crypto from 'crypto'
|
|
11
12
|
import fs from 'fs'
|
|
12
13
|
import { createRequire } from 'module'
|
|
13
14
|
import path from 'path'
|
|
14
|
-
import { fileURLToPath } from 'url'
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
15
16
|
import { type Plugin, transformWithOxc } from 'vite'
|
|
17
|
+
|
|
18
|
+
declare module '@babel/core' {
|
|
19
|
+
interface TransformCaller {
|
|
20
|
+
platform?: string
|
|
21
|
+
isDev?: boolean
|
|
22
|
+
projectRoot?: string
|
|
23
|
+
}
|
|
24
|
+
}
|
|
16
25
|
import { resolveExportsHiddenSubpath } from '../../peach-compat/src/resolve-hidden-subpath.ts'
|
|
17
26
|
import {
|
|
18
27
|
SOOTSIM_COMPAT_PUBLISHED_BROWSER_ENTRIES,
|
|
@@ -352,6 +361,10 @@ export function sootsim(options: SootSimOptions = {}): Plugin[] {
|
|
|
352
361
|
keyboardControllerBindingsRedirect(),
|
|
353
362
|
reanimatedNativeSeamRedirect(),
|
|
354
363
|
manifestNativeSeamRedirect(),
|
|
364
|
+
expoDomComponentsPlugin({
|
|
365
|
+
appDir,
|
|
366
|
+
projectRoot: appDir || workspaceRoot,
|
|
367
|
+
}),
|
|
355
368
|
workletsBabelTransform(isAppSource),
|
|
356
369
|
metroNativeResolve(isAppSource),
|
|
357
370
|
reactNativeRequirePlugin(isAppSource),
|
|
@@ -439,6 +452,7 @@ export function engineShellWorkerPlugins(): Plugin[] {
|
|
|
439
452
|
nodeModulesJsxTransform(),
|
|
440
453
|
recordingPluginWorkerRedirect(),
|
|
441
454
|
reanimatedNativeSeamRedirect(),
|
|
455
|
+
gestureHandlerReanimatedRequire(),
|
|
442
456
|
manifestNativeSeamRedirect(),
|
|
443
457
|
keyboardControllerBindingsRedirect(),
|
|
444
458
|
workletsBabelTransform(() => true),
|
|
@@ -540,6 +554,9 @@ export function workerBootChunkPlugin(closures: WorkerBootClosure[]): Plugin {
|
|
|
540
554
|
const chunk = chunks[0]
|
|
541
555
|
const pkgs = new Map<string, Set<string>>()
|
|
542
556
|
for (const id of chunk.moduleIds) {
|
|
557
|
+
// a virtual `\0` id (the jsx-runtime re-export shim) names a
|
|
558
|
+
// node_modules path without being a second copy of that package
|
|
559
|
+
if (id.startsWith('\0')) continue
|
|
543
560
|
const nmIndex = id.lastIndexOf('/node_modules/')
|
|
544
561
|
if (nmIndex !== -1) {
|
|
545
562
|
const rest = id.slice(nmIndex + '/node_modules/'.length)
|
|
@@ -562,14 +579,81 @@ export function workerBootChunkPlugin(closures: WorkerBootClosure[]): Plugin {
|
|
|
562
579
|
duplicates.length > 0
|
|
563
580
|
? `\ndetected duplicate packages bundled from multiple roots (likely a symlinked or cross-tree node_modules):\n ${duplicates.join('\n ')}`
|
|
564
581
|
: ''
|
|
582
|
+
// the largest modules are where a cut pays, so the error names them
|
|
583
|
+
const largest = Object.entries(chunk.modules)
|
|
584
|
+
.sort(([, a], [, b]) => b.renderedLength - a.renderedLength)
|
|
585
|
+
.slice(0, 40)
|
|
586
|
+
.map(
|
|
587
|
+
([id, module]) =>
|
|
588
|
+
`${module.renderedLength} ${path.relative(process.cwd(), id)}`,
|
|
589
|
+
)
|
|
565
590
|
this.error(
|
|
566
|
-
`worker ${closure.entry}: ${chunks[0].fileName} is ${bytes} bytes, over the ${closure.maxBytes} byte cap; eliminate excess boot dependencies rather than raising the cap${dupDetail}`,
|
|
591
|
+
`worker ${closure.entry}: ${chunks[0].fileName} is ${bytes} bytes, over the ${closure.maxBytes} byte cap; eliminate excess boot dependencies rather than raising the cap${dupDetail}\nlargest modules (rendered bytes):\n ${largest.join('\n ')}`,
|
|
567
592
|
)
|
|
568
593
|
}
|
|
569
594
|
},
|
|
570
595
|
}
|
|
571
596
|
}
|
|
572
597
|
|
|
598
|
+
// react-native-gesture-handler reaches reanimated through an optional
|
|
599
|
+
// `require('react-native-reanimated')` in reanimatedWrapper. a require of an
|
|
600
|
+
// ES module binds its whole namespace, so every reanimated export (the
|
|
601
|
+
// layout-animation presets alone were ~230kB) stayed in each engine worker
|
|
602
|
+
// file although gesture-handler reads a handful of members. resolve that one
|
|
603
|
+
// require to a module re-exporting exactly the members gesture-handler's
|
|
604
|
+
// sources read off its `Reanimated` binding, scanned from the installed
|
|
605
|
+
// package so an upgrade that reads another member keeps it.
|
|
606
|
+
export function gestureHandlerReanimatedRequire(): Plugin {
|
|
607
|
+
const virtualId = '\0sootsim:gesture-handler-reanimated'
|
|
608
|
+
const packageMarker = '/node_modules/react-native-gesture-handler/'
|
|
609
|
+
let sourceDir: string | null = null
|
|
610
|
+
const readMembers = (dir: string, members: Set<string>) => {
|
|
611
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
612
|
+
const entryPath = path.join(dir, entry.name)
|
|
613
|
+
if (entry.isDirectory()) {
|
|
614
|
+
readMembers(entryPath, members)
|
|
615
|
+
} else if (/\.[jt]sx?$/.test(entry.name)) {
|
|
616
|
+
for (const match of fs
|
|
617
|
+
.readFileSync(entryPath, 'utf8')
|
|
618
|
+
.matchAll(/\bReanimated\??\.([A-Za-z_$][\w$]*)/g)) {
|
|
619
|
+
members.add(match[1])
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
name: 'sootsim-gesture-handler-reanimated-require',
|
|
626
|
+
apply: 'build',
|
|
627
|
+
enforce: 'pre',
|
|
628
|
+
resolveId: {
|
|
629
|
+
filter: { id: /^react-native-reanimated$/ },
|
|
630
|
+
handler(_source, importer) {
|
|
631
|
+
if (!importer || !/\/reanimatedWrapper\.[jt]s$/.test(importer)) return null
|
|
632
|
+
const markerIndex = importer.indexOf(packageMarker)
|
|
633
|
+
if (markerIndex === -1) return null
|
|
634
|
+
const packageRoot = importer.slice(0, markerIndex + packageMarker.length)
|
|
635
|
+
// the tree the importer resolved from (src or lib), not every build
|
|
636
|
+
sourceDir = path.join(
|
|
637
|
+
packageRoot,
|
|
638
|
+
importer.slice(packageRoot.length).split('/')[0],
|
|
639
|
+
)
|
|
640
|
+
return virtualId
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
load(id) {
|
|
644
|
+
if (id !== virtualId || !sourceDir) return null
|
|
645
|
+
const members = new Set<string>()
|
|
646
|
+
readMembers(sourceDir, members)
|
|
647
|
+
if (members.size === 0) {
|
|
648
|
+
this.error(
|
|
649
|
+
`gesture-handler reanimated require: no \`Reanimated.<member>\` reads under ${sourceDir}; its reanimatedWrapper changed shape, so update this plugin`,
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
return `export { ${[...members].sort().join(', ')} } from 'react-native-reanimated'\n`
|
|
653
|
+
},
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
573
657
|
// react-native-keyboard-controller is pure-JS for everything except its
|
|
574
658
|
// native-event seam (`bindings.ts` / `bindings.native.ts`) and RN platform
|
|
575
659
|
// `findNodeHandle` resolution. let upstream's components, hooks, animated
|
|
@@ -768,26 +852,20 @@ export function reanimatedNativeSeamRedirect(): Plugin {
|
|
|
768
852
|
// use the manifest path unless they need similar special handling.
|
|
769
853
|
import { NATIVE_SEAM_MANIFEST } from './native-seam-manifest.ts'
|
|
770
854
|
export function manifestNativeSeamRedirect(): Plugin {
|
|
771
|
-
//
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
{ basenames: Set<string>; target: string; notes?: string }
|
|
775
|
-
>()
|
|
855
|
+
// pkg → basename → seam. one package can split its native surface across
|
|
856
|
+
// several upstream files, each redirected to its own seam.
|
|
857
|
+
const byPkg = new Map<string, Map<string, { target: string; notes?: string }>>()
|
|
776
858
|
for (const entry of NATIVE_SEAM_MANIFEST) {
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
byPkg.set(entry.pkg, {
|
|
782
|
-
basenames: new Set(entry.seamBasenames),
|
|
783
|
-
target: entry.target,
|
|
784
|
-
notes: entry.notes,
|
|
785
|
-
})
|
|
859
|
+
const seams = byPkg.get(entry.pkg) ?? new Map()
|
|
860
|
+
byPkg.set(entry.pkg, seams)
|
|
861
|
+
for (const basename of entry.seamBasenames) {
|
|
862
|
+
seams.set(basename, { target: entry.target, notes: entry.notes })
|
|
786
863
|
}
|
|
787
864
|
}
|
|
865
|
+
const targets = new Set(NATIVE_SEAM_MANIFEST.map((entry) => entry.target))
|
|
788
866
|
const sourceFilter = new RegExp(
|
|
789
867
|
`(?:^|/)(?:${[...byPkg.values()]
|
|
790
|
-
.flatMap((
|
|
868
|
+
.flatMap((seams) => [...seams.keys()])
|
|
791
869
|
.map((basename) => basename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
792
870
|
.join('|')})(?:\\.(?:native|web|ios|android))?$`,
|
|
793
871
|
)
|
|
@@ -797,26 +875,26 @@ export function manifestNativeSeamRedirect(): Plugin {
|
|
|
797
875
|
resolveId: {
|
|
798
876
|
filter: { id: sourceFilter },
|
|
799
877
|
handler(source, importer) {
|
|
800
|
-
if (!importer || byPkg.size === 0) return null
|
|
878
|
+
if (!importer || byPkg.size === 0 || targets.has(importer)) return null
|
|
801
879
|
// find the package whose path appears in the importer. matches both
|
|
802
880
|
// `/<pkg>/` (unscoped) and `/@scope/<pkg>/` (scoped — the leading `@`
|
|
803
881
|
// is part of the pkg name in the manifest, e.g. `@notifee/react-native`).
|
|
804
|
-
let
|
|
882
|
+
let seams: Map<string, { target: string; notes?: string }> | undefined
|
|
805
883
|
let matchedPkg: string | undefined
|
|
806
884
|
for (const [pkg, entry] of byPkg) {
|
|
807
885
|
if (importer.includes(`/${pkg}/`)) {
|
|
808
|
-
|
|
886
|
+
seams = entry
|
|
809
887
|
matchedPkg = pkg
|
|
810
888
|
break
|
|
811
889
|
}
|
|
812
890
|
}
|
|
813
|
-
if (!
|
|
814
|
-
if (importer === match.target) return null
|
|
891
|
+
if (!seams) return null
|
|
815
892
|
const basename = (source.split('/').pop() ?? '').replace(
|
|
816
893
|
/\.(native|web|ios|android)$/,
|
|
817
894
|
'',
|
|
818
895
|
)
|
|
819
|
-
|
|
896
|
+
const seam = seams.get(basename)
|
|
897
|
+
if (!seam) return null
|
|
820
898
|
if (process.env.SOOTSIM_NATIVE_SEAM_DEBUG) {
|
|
821
899
|
console.log(
|
|
822
900
|
'[nativeSeamRedirect]',
|
|
@@ -825,10 +903,10 @@ export function manifestNativeSeamRedirect(): Plugin {
|
|
|
825
903
|
source,
|
|
826
904
|
'from',
|
|
827
905
|
importer.slice(-80),
|
|
828
|
-
|
|
906
|
+
seam.notes ? `(${seam.notes})` : '',
|
|
829
907
|
)
|
|
830
908
|
}
|
|
831
|
-
return
|
|
909
|
+
return seam.target
|
|
832
910
|
},
|
|
833
911
|
},
|
|
834
912
|
}
|
|
@@ -1931,3 +2009,623 @@ function stubMissingImages(isAppSource: (p: string) => boolean): Plugin {
|
|
|
1931
2009
|
},
|
|
1932
2010
|
}
|
|
1933
2011
|
}
|
|
2012
|
+
|
|
2013
|
+
let domBabelCorePromise: Promise<typeof import('@babel/core')> | null = null
|
|
2014
|
+
async function getDomBabelCore() {
|
|
2015
|
+
if (!domBabelCorePromise) {
|
|
2016
|
+
domBabelCorePromise = import('@babel/core') as Promise<typeof import('@babel/core')>
|
|
2017
|
+
}
|
|
2018
|
+
return domBabelCorePromise
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
export function expoDomComponentsPlugin(
|
|
2022
|
+
options: {
|
|
2023
|
+
appDir?: string
|
|
2024
|
+
projectRoot?: string
|
|
2025
|
+
} = {},
|
|
2026
|
+
): Plugin {
|
|
2027
|
+
let isProduction = false
|
|
2028
|
+
const appDir = options.appDir ? path.resolve(options.appDir) : ''
|
|
2029
|
+
const projectRoot = options.projectRoot ?? appDir ?? workspaceRoot
|
|
2030
|
+
const domHashMap = new Map<string, string>()
|
|
2031
|
+
const domWebModuleIds = new Set<string>()
|
|
2032
|
+
|
|
2033
|
+
// specifiers that must stay runtime requires served by the page's
|
|
2034
|
+
// window.require shim instead of becoming static imports:
|
|
2035
|
+
// - expo's log-box is native-only; importing it would pull react-native
|
|
2036
|
+
// into the web closure (mirrors the @expo/log-box virtual stub).
|
|
2037
|
+
// - the react-native-web stylesheets are an optional integration whose
|
|
2038
|
+
// callers fall back when require returns an empty object.
|
|
2039
|
+
const domRuntimeRequireSpecifiers = new Set([
|
|
2040
|
+
'@expo/log-box',
|
|
2041
|
+
'@expo/log-box/lib',
|
|
2042
|
+
'react-native-web/dist/exports/StyleSheet',
|
|
2043
|
+
'react-native-web/dist/exports/StyleSheet/dom',
|
|
2044
|
+
// metro fast-refresh plumbing cannot work under vite (no metro server,
|
|
2045
|
+
// no hmr client socket); the page gets hmr from vite's own client.
|
|
2046
|
+
// importing the real module would also fork the engine's refresh
|
|
2047
|
+
// identity, so serve a no-op shape instead.
|
|
2048
|
+
'react-refresh/runtime',
|
|
2049
|
+
])
|
|
2050
|
+
|
|
2051
|
+
// expo's dom web closure is written for metro, where require() exists in
|
|
2052
|
+
// every module. vite serves ESM, so a bare require is a ReferenceError
|
|
2053
|
+
// (and the entry module that defines window.require evaluates after its
|
|
2054
|
+
// static imports). rewrite static requires in closure modules to ESM
|
|
2055
|
+
// imports at serve time; this only changes module format, never behavior.
|
|
2056
|
+
function rewriteDomWebRequires(code: string): string | null {
|
|
2057
|
+
const pattern = /(^|[^.\w$])require\(\s*('[^']*'|"[^"]*")\s*\)/gm
|
|
2058
|
+
const names = new Map<string, string>()
|
|
2059
|
+
let found = false
|
|
2060
|
+
for (const match of code.matchAll(pattern)) {
|
|
2061
|
+
const spec = match[2].slice(1, -1)
|
|
2062
|
+
if (domRuntimeRequireSpecifiers.has(spec)) continue
|
|
2063
|
+
found = true
|
|
2064
|
+
if (!names.has(spec)) names.set(spec, `__domRequire_${names.size}`)
|
|
2065
|
+
}
|
|
2066
|
+
if (!found) return null
|
|
2067
|
+
const body = code.replace(pattern, (full, prefix, quoted: string) => {
|
|
2068
|
+
const spec = quoted.slice(1, -1)
|
|
2069
|
+
if (domRuntimeRequireSpecifiers.has(spec)) return full
|
|
2070
|
+
const name = names.get(spec)!
|
|
2071
|
+
return `${prefix}(${name}.default ?? ${name})`
|
|
2072
|
+
})
|
|
2073
|
+
const imports = [...names]
|
|
2074
|
+
.map(([spec, name]) => `import * as ${name} from ${JSON.stringify(spec)};`)
|
|
2075
|
+
.join('\n')
|
|
2076
|
+
return `${imports}\n${body}`
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
function isDomEntryId(id: string): boolean {
|
|
2080
|
+
const clean = id.replace(/\?.*$/, '')
|
|
2081
|
+
return (
|
|
2082
|
+
clean === `${virtualIdPrefix}_expo_dom_entry` ||
|
|
2083
|
+
clean === '\\0_expo_dom_entry' ||
|
|
2084
|
+
clean === '_expo_dom_entry'
|
|
2085
|
+
)
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function isDomWebModule(id: string, importer?: string): boolean {
|
|
2089
|
+
// the web closure is rooted at the dom entry: only modules reachable
|
|
2090
|
+
// from it take web resolution. never classify by path substring: native
|
|
2091
|
+
// modules such as @expo/dom-webview sources contain 'dom-web' but must
|
|
2092
|
+
// keep native resolution (their react-native import must not rewrite to
|
|
2093
|
+
// react-native-web, which unmounts the native webview).
|
|
2094
|
+
if (isDomEntryId(id)) return true
|
|
2095
|
+
if (!importer) return false
|
|
2096
|
+
const cleanImporter = importer.replace(/\?.*$/, '')
|
|
2097
|
+
return domWebModuleIds.has(cleanImporter) || domWebModuleIds.has(importer)
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
return {
|
|
2101
|
+
name: 'sootsim-expo-dom-components',
|
|
2102
|
+
enforce: 'pre',
|
|
2103
|
+
config() {
|
|
2104
|
+
// the dom web closure imports CJS-only packages (react, expo's
|
|
2105
|
+
// metro-hmr devtooling). vite's CJS→ESM interop only kicks in via
|
|
2106
|
+
// pre-bundle, and lazily reached page graphs cannot rely on
|
|
2107
|
+
// first-request optimization, so force them here. nothing outside
|
|
2108
|
+
// this closure imports pretty-format or log-box-utils.
|
|
2109
|
+
// react-refresh/runtime is deliberately absent: the engine imports
|
|
2110
|
+
// it directly and its module identity is load-bearing for fast
|
|
2111
|
+
// refresh, so the closure shims it instead (see below).
|
|
2112
|
+
return {
|
|
2113
|
+
optimizeDeps: {
|
|
2114
|
+
include: [
|
|
2115
|
+
'react',
|
|
2116
|
+
'react-dom',
|
|
2117
|
+
'react-dom/client',
|
|
2118
|
+
'react/jsx-runtime',
|
|
2119
|
+
'react/jsx-dev-runtime',
|
|
2120
|
+
'pretty-format',
|
|
2121
|
+
'@expo/log-box-utils',
|
|
2122
|
+
],
|
|
2123
|
+
},
|
|
2124
|
+
}
|
|
2125
|
+
},
|
|
2126
|
+
configResolved(config) {
|
|
2127
|
+
isProduction =
|
|
2128
|
+
(config.mode === 'production' || config.isProduction) &&
|
|
2129
|
+
process.env.SOOTSIM_TENANT_BUILD_MODE !== 'development'
|
|
2130
|
+
},
|
|
2131
|
+
configureServer(server) {
|
|
2132
|
+
server.middlewares.use((req, res, next) => {
|
|
2133
|
+
if (!req.url) return next()
|
|
2134
|
+
const rawUrl = req.url
|
|
2135
|
+
const isDomRoute =
|
|
2136
|
+
rawUrl.startsWith('/_expo/@dom') ||
|
|
2137
|
+
rawUrl.startsWith('/www.bundle') ||
|
|
2138
|
+
rawUrl.startsWith('www.bundle')
|
|
2139
|
+
if (!isDomRoute) return next()
|
|
2140
|
+
|
|
2141
|
+
const url = new URL(
|
|
2142
|
+
rawUrl.startsWith('/') ? rawUrl : '/' + rawUrl,
|
|
2143
|
+
'http://localhost',
|
|
2144
|
+
)
|
|
2145
|
+
if (url.pathname === '/_expo/@dom/entry' || url.pathname.endsWith('/entry')) {
|
|
2146
|
+
return next()
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
let fileUrl = url.searchParams.get('file')
|
|
2150
|
+
if (
|
|
2151
|
+
!fileUrl &&
|
|
2152
|
+
(url.pathname.includes('/www.bundle/') ||
|
|
2153
|
+
url.pathname.startsWith('/www.bundle'))
|
|
2154
|
+
) {
|
|
2155
|
+
const match = url.pathname.match(/([a-f0-9]{32})\.html/i)
|
|
2156
|
+
if (match && domHashMap.has(match[1])) {
|
|
2157
|
+
fileUrl = domHashMap.get(match[1])!
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
if (!fileUrl) return next()
|
|
2161
|
+
|
|
2162
|
+
let decodedFilePath: string
|
|
2163
|
+
try {
|
|
2164
|
+
decodedFilePath = fileUrl.startsWith('file:') ? fileURLToPath(fileUrl) : fileUrl
|
|
2165
|
+
} catch {
|
|
2166
|
+
decodedFilePath = fileUrl
|
|
2167
|
+
}
|
|
2168
|
+
const title = path.basename(decodedFilePath)
|
|
2169
|
+
const entryScriptSrc = `/_expo/@dom/entry?file=${encodeURIComponent(fileUrl)}`
|
|
2170
|
+
|
|
2171
|
+
const html = `<!DOCTYPE html>
|
|
2172
|
+
<html>
|
|
2173
|
+
<head>
|
|
2174
|
+
<meta charset="utf-8" />
|
|
2175
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
|
2176
|
+
<title>${title.replace(/&/g, '&').replace(/"/g, '"')}</title>
|
|
2177
|
+
<style id="expo-dom-component-style">
|
|
2178
|
+
html, body {
|
|
2179
|
+
-webkit-overflow-scrolling: touch;
|
|
2180
|
+
margin: 0;
|
|
2181
|
+
padding: 0;
|
|
2182
|
+
width: 100%;
|
|
2183
|
+
height: 100%;
|
|
2184
|
+
}
|
|
2185
|
+
#root {
|
|
2186
|
+
display: flex;
|
|
2187
|
+
flex: 1;
|
|
2188
|
+
width: 100%;
|
|
2189
|
+
height: 100%;
|
|
2190
|
+
}
|
|
2191
|
+
</style>
|
|
2192
|
+
<script>
|
|
2193
|
+
window.__DEV__ = true;
|
|
2194
|
+
window.process = window.process || { env: { NODE_ENV: 'development' } };
|
|
2195
|
+
// expo's dom closure branches on EXPO_OS (hmr setup takes the web
|
|
2196
|
+
// form only when it is 'web'); metro sets this at bundle time.
|
|
2197
|
+
window.process.env = window.process.env || {};
|
|
2198
|
+
window.process.env.NODE_ENV = window.process.env.NODE_ENV || 'development';
|
|
2199
|
+
window.process.env.EXPO_OS = 'web';
|
|
2200
|
+
(function () {
|
|
2201
|
+
var documentUrl = location.href;
|
|
2202
|
+
function frameUrl() { return String(location.href); }
|
|
2203
|
+
window.ReactNativeWebView = window.ReactNativeWebView || {
|
|
2204
|
+
postMessage: function (data) {
|
|
2205
|
+
window.parent.postMessage({
|
|
2206
|
+
__sootsimWebViewKind: 'reactNativeWebViewMessage',
|
|
2207
|
+
data: String(data),
|
|
2208
|
+
url: frameUrl(),
|
|
2209
|
+
loading: document.readyState !== 'complete',
|
|
2210
|
+
title: String(document.title || '')
|
|
2211
|
+
}, '*');
|
|
2212
|
+
},
|
|
2213
|
+
injectedObjectJson: function () {
|
|
2214
|
+
try {
|
|
2215
|
+
if (window.parent && window.parent.__sootsimGetInjectedObjectJson) {
|
|
2216
|
+
var res = window.parent.__sootsimGetInjectedObjectJson(frameUrl());
|
|
2217
|
+
if (res) return typeof res === 'string' ? res : JSON.stringify(res);
|
|
2218
|
+
}
|
|
2219
|
+
} catch (e) {}
|
|
2220
|
+
return "{}";
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
window.addEventListener('message', function (event) {
|
|
2224
|
+
if (event.source !== window.parent) return;
|
|
2225
|
+
var payload = event.data;
|
|
2226
|
+
if (!payload || typeof payload !== 'object') return;
|
|
2227
|
+
if (payload.__sootsimWebViewKind !== 'injectJavaScript') return;
|
|
2228
|
+
event.stopImmediatePropagation();
|
|
2229
|
+
try {
|
|
2230
|
+
(0, eval)(String(payload.js || ''));
|
|
2231
|
+
} catch (err) {
|
|
2232
|
+
setTimeout(function () { throw err; }, 0);
|
|
2233
|
+
}
|
|
2234
|
+
}, true);
|
|
2235
|
+
// metro-style require shim for expo's dom closure: classic scripts
|
|
2236
|
+
// run before module imports evaluate, so this covers eval-time
|
|
2237
|
+
// requires. static requires are rewritten to imports at serve time;
|
|
2238
|
+
// only denylisted specifiers (native-only log-box, optional
|
|
2239
|
+
// stylesheets) reach this. the entry module installs the fuller
|
|
2240
|
+
// version below once react is loaded.
|
|
2241
|
+
window.require = window.require || function (mod) {
|
|
2242
|
+
if (mod === '@expo/log-box' || mod === '@expo/log-box/lib') {
|
|
2243
|
+
return { setupLogBox: function () {} };
|
|
2244
|
+
}
|
|
2245
|
+
if (mod === 'react-refresh/runtime') {
|
|
2246
|
+
return {
|
|
2247
|
+
injectIntoGlobalHook: function () {},
|
|
2248
|
+
createSignatureFunctionForTransform: function () { return function (t) { return t; }; },
|
|
2249
|
+
isLikelyComponentType: function () { return false; },
|
|
2250
|
+
getFamilyByType: function () { return undefined; },
|
|
2251
|
+
register: function () {},
|
|
2252
|
+
performReactRefresh: function () {},
|
|
2253
|
+
hasUnrecoverableErrors: function () { return false; }
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
return {};
|
|
2257
|
+
};
|
|
2258
|
+
})();
|
|
2259
|
+
</script>
|
|
2260
|
+
<script>
|
|
2261
|
+
var injectedObject = {};
|
|
2262
|
+
try {
|
|
2263
|
+
injectedObject = JSON.parse(window.ReactNativeWebView.injectedObjectJson());
|
|
2264
|
+
} catch (e) {
|
|
2265
|
+
console.warn('Failed to parse injectedObjectJson:', e);
|
|
2266
|
+
}
|
|
2267
|
+
window.$$EXPO_DOM_HOST_OS = injectedObject.EXPO_DOM_HOST_OS || 'ios';
|
|
2268
|
+
window.$$EXPO_INITIAL_PROPS = injectedObject.initialProps || { names: [], props: {} };
|
|
2269
|
+
</script>
|
|
2270
|
+
</head>
|
|
2271
|
+
<body>
|
|
2272
|
+
<noscript>DOM Components require <code>javaScriptEnabled</code></noscript>
|
|
2273
|
+
<div id="root"></div>
|
|
2274
|
+
<script type="module" src="/@vite/client"></script>
|
|
2275
|
+
<script type="module" src="${entryScriptSrc}"></script>
|
|
2276
|
+
</body>
|
|
2277
|
+
</html>`
|
|
2278
|
+
res.statusCode = 200
|
|
2279
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
2280
|
+
res.setHeader('Cache-Control', 'no-cache')
|
|
2281
|
+
// the shell page is cross-origin isolated (COOP same-origin plus
|
|
2282
|
+
// COEP require-corp): a same-origin iframe document without matching
|
|
2283
|
+
// response headers is blocked before any script runs. mirror the
|
|
2284
|
+
// engine-tenant static handler.
|
|
2285
|
+
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
|
|
2286
|
+
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin')
|
|
2287
|
+
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
|
|
2288
|
+
res.end(html)
|
|
2289
|
+
})
|
|
2290
|
+
},
|
|
2291
|
+
async resolveId(id, importer) {
|
|
2292
|
+
// expo's ExpoDOMWebView and RNWebView wrappers require their package in
|
|
2293
|
+
// a try/catch and export undefined when it is not installed, so the
|
|
2294
|
+
// webview wrapper can report the missing package it chose. esm cannot
|
|
2295
|
+
// catch a static import, so resolve the package here and answer with
|
|
2296
|
+
// the same undefined when it is absent.
|
|
2297
|
+
const optionalWebViewReexport = async (pkg: string, from: string | undefined) => {
|
|
2298
|
+
const resolved = await this.resolve(pkg, from, { skipSelf: true })
|
|
2299
|
+
return `${virtualIdPrefix}optional-webview:${pkg}${resolved ? '' : '?missing'}`
|
|
2300
|
+
}
|
|
2301
|
+
const inDomWeb = importer == null || isDomWebModule(id, importer)
|
|
2302
|
+
if (
|
|
2303
|
+
(id === '@expo/metro-runtime' ||
|
|
2304
|
+
id.startsWith('@expo/metro-runtime/') ||
|
|
2305
|
+
id.startsWith('@expo/metro/')) &&
|
|
2306
|
+
inDomWeb
|
|
2307
|
+
) {
|
|
2308
|
+
return `${virtualIdPrefix}@expo/metro-runtime`
|
|
2309
|
+
}
|
|
2310
|
+
if ((id === '@expo/log-box' || id === '@expo/log-box/lib') && inDomWeb) {
|
|
2311
|
+
return `${virtualIdPrefix}@expo/log-box`
|
|
2312
|
+
}
|
|
2313
|
+
if (
|
|
2314
|
+
(id === 'expo/build/dom/webview/ExpoDOMWebView' ||
|
|
2315
|
+
id.endsWith('/expo/build/dom/webview/ExpoDOMWebView.js') ||
|
|
2316
|
+
id.endsWith('/expo/build/dom/webview/ExpoDOMWebView')) &&
|
|
2317
|
+
inDomWeb
|
|
2318
|
+
) {
|
|
2319
|
+
return optionalWebViewReexport('@expo/dom-webview', importer)
|
|
2320
|
+
}
|
|
2321
|
+
if (
|
|
2322
|
+
(id === 'expo/build/dom/webview/RNWebView' ||
|
|
2323
|
+
id.endsWith('/expo/build/dom/webview/RNWebView.js') ||
|
|
2324
|
+
id.endsWith('/expo/build/dom/webview/RNWebView')) &&
|
|
2325
|
+
inDomWeb
|
|
2326
|
+
) {
|
|
2327
|
+
return optionalWebViewReexport('react-native-webview', importer)
|
|
2328
|
+
}
|
|
2329
|
+
if (
|
|
2330
|
+
id.startsWith('/_expo/@dom/entry') ||
|
|
2331
|
+
id.startsWith('@dom-entry:') ||
|
|
2332
|
+
id.includes('/_expo/@dom/entry')
|
|
2333
|
+
) {
|
|
2334
|
+
const query = id.includes('?') ? id.slice(id.indexOf('?')) : ''
|
|
2335
|
+
const resolved = `${virtualIdPrefix}_expo_dom_entry${query}`
|
|
2336
|
+
domWebModuleIds.add(virtualIdPrefix + '_expo_dom_entry')
|
|
2337
|
+
domWebModuleIds.add('\0_expo_dom_entry')
|
|
2338
|
+
domWebModuleIds.add('_expo_dom_entry')
|
|
2339
|
+
return resolved
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
if (isDomWebModule(id, importer)) {
|
|
2343
|
+
if (id === 'expo/dom/internal') {
|
|
2344
|
+
const resolved = path.resolve(
|
|
2345
|
+
workspaceNodeModules,
|
|
2346
|
+
'expo/build/dom/internal/internal.web.js',
|
|
2347
|
+
)
|
|
2348
|
+
domWebModuleIds.add(resolved)
|
|
2349
|
+
return resolved
|
|
2350
|
+
}
|
|
2351
|
+
if (id === 'expo/dom') {
|
|
2352
|
+
const resolved = path.resolve(
|
|
2353
|
+
workspaceNodeModules,
|
|
2354
|
+
'expo/build/dom/dom/dom.web.js',
|
|
2355
|
+
)
|
|
2356
|
+
domWebModuleIds.add(resolved)
|
|
2357
|
+
return resolved
|
|
2358
|
+
}
|
|
2359
|
+
if (id === 'react-native') {
|
|
2360
|
+
const resolved = path.resolve(
|
|
2361
|
+
workspaceNodeModules,
|
|
2362
|
+
'react-native-web/dist/cjs/index.js',
|
|
2363
|
+
)
|
|
2364
|
+
domWebModuleIds.add(resolved)
|
|
2365
|
+
return resolved
|
|
2366
|
+
}
|
|
2367
|
+
if (
|
|
2368
|
+
id.endsWith('/launch/registerRootComponent') ||
|
|
2369
|
+
id === '../launch/registerRootComponent'
|
|
2370
|
+
) {
|
|
2371
|
+
const resolved = path.resolve(
|
|
2372
|
+
workspaceNodeModules,
|
|
2373
|
+
'expo/build/launch/registerRootComponent.web.js',
|
|
2374
|
+
)
|
|
2375
|
+
domWebModuleIds.add(resolved)
|
|
2376
|
+
return resolved
|
|
2377
|
+
}
|
|
2378
|
+
if (id.startsWith('.') && importer) {
|
|
2379
|
+
const dir = path.dirname(importer.replace(/\?.*$/, ''))
|
|
2380
|
+
const resolved = path.resolve(dir, id)
|
|
2381
|
+
if (fs.existsSync(resolved)) {
|
|
2382
|
+
try {
|
|
2383
|
+
const stat = fs.statSync(resolved)
|
|
2384
|
+
if (stat.isFile()) {
|
|
2385
|
+
domWebModuleIds.add(resolved)
|
|
2386
|
+
return resolved
|
|
2387
|
+
}
|
|
2388
|
+
if (stat.isDirectory()) {
|
|
2389
|
+
const indexCandidates = [
|
|
2390
|
+
path.join(resolved, 'index.web.tsx'),
|
|
2391
|
+
path.join(resolved, 'index.web.ts'),
|
|
2392
|
+
path.join(resolved, 'index.web.jsx'),
|
|
2393
|
+
path.join(resolved, 'index.web.js'),
|
|
2394
|
+
path.join(resolved, 'index.tsx'),
|
|
2395
|
+
path.join(resolved, 'index.ts'),
|
|
2396
|
+
path.join(resolved, 'index.jsx'),
|
|
2397
|
+
path.join(resolved, 'index.js'),
|
|
2398
|
+
]
|
|
2399
|
+
for (const candidate of indexCandidates) {
|
|
2400
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
2401
|
+
domWebModuleIds.add(candidate)
|
|
2402
|
+
return candidate
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
} catch {}
|
|
2407
|
+
}
|
|
2408
|
+
const webExtensions = [
|
|
2409
|
+
'.web.tsx',
|
|
2410
|
+
'.web.ts',
|
|
2411
|
+
'.web.jsx',
|
|
2412
|
+
'.web.js',
|
|
2413
|
+
'.tsx',
|
|
2414
|
+
'.ts',
|
|
2415
|
+
'.jsx',
|
|
2416
|
+
'.js',
|
|
2417
|
+
]
|
|
2418
|
+
for (const ext of webExtensions) {
|
|
2419
|
+
const candidate =
|
|
2420
|
+
resolved.endsWith('.js') && ext.startsWith('.web.')
|
|
2421
|
+
? resolved.slice(0, -3) + ext
|
|
2422
|
+
: resolved + ext
|
|
2423
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
2424
|
+
domWebModuleIds.add(candidate)
|
|
2425
|
+
return candidate
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
const indexCandidates = [
|
|
2429
|
+
path.join(resolved, 'index.web.tsx'),
|
|
2430
|
+
path.join(resolved, 'index.web.ts'),
|
|
2431
|
+
path.join(resolved, 'index.web.jsx'),
|
|
2432
|
+
path.join(resolved, 'index.web.js'),
|
|
2433
|
+
path.join(resolved, 'index.tsx'),
|
|
2434
|
+
path.join(resolved, 'index.ts'),
|
|
2435
|
+
path.join(resolved, 'index.jsx'),
|
|
2436
|
+
path.join(resolved, 'index.js'),
|
|
2437
|
+
]
|
|
2438
|
+
for (const candidate of indexCandidates) {
|
|
2439
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
2440
|
+
domWebModuleIds.add(candidate)
|
|
2441
|
+
return candidate
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
// bare specifiers must keep vite core resolution: resolving them
|
|
2446
|
+
// here would pin an absolute /@fs id that bypasses dep pre-bundling,
|
|
2447
|
+
// serving raw CJS with no named/default exports in the browser.
|
|
2448
|
+
// (relative ids above keep our metro-style .web preference.)
|
|
2449
|
+
const isBare =
|
|
2450
|
+
!id.startsWith('.') &&
|
|
2451
|
+
!id.startsWith('/') &&
|
|
2452
|
+
!id.startsWith('\0') &&
|
|
2453
|
+
!id.includes('://') &&
|
|
2454
|
+
!path.isAbsolute(id)
|
|
2455
|
+
if (isBare) return null
|
|
2456
|
+
const fallback = await this.resolve(id, importer, { skipSelf: true })
|
|
2457
|
+
if (fallback) {
|
|
2458
|
+
domWebModuleIds.add(fallback.id.replace(/\?.*$/, ''))
|
|
2459
|
+
return fallback
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
return null
|
|
2463
|
+
},
|
|
2464
|
+
load(id) {
|
|
2465
|
+
if (id === `${virtualIdPrefix}@expo/metro-runtime`) {
|
|
2466
|
+
// metro's hmr client cannot work under vite: there is no metro
|
|
2467
|
+
// server or hot socket, and vite's own client provides reload.
|
|
2468
|
+
// expo's hmr module constructs one at startup and calls
|
|
2469
|
+
// on/send/isEnabled/enable/disable/hasPendingUpdates/close, so
|
|
2470
|
+
// serve a no-op with that surface instead of connecting anywhere.
|
|
2471
|
+
return `class NoopMetroHMRClient {
|
|
2472
|
+
on() { return () => {}; }
|
|
2473
|
+
send() {}
|
|
2474
|
+
isEnabled() { return false; }
|
|
2475
|
+
hasPendingUpdates() { return false; }
|
|
2476
|
+
enable() {}
|
|
2477
|
+
disable() {}
|
|
2478
|
+
close() {}
|
|
2479
|
+
}
|
|
2480
|
+
export default NoopMetroHMRClient;
|
|
2481
|
+
`
|
|
2482
|
+
}
|
|
2483
|
+
if (id === `${virtualIdPrefix}@expo/log-box`) {
|
|
2484
|
+
return 'export function setupLogBox() {}; export default { setupLogBox() {} };'
|
|
2485
|
+
}
|
|
2486
|
+
if (id.startsWith(`${virtualIdPrefix}optional-webview:`)) {
|
|
2487
|
+
const pkg = id
|
|
2488
|
+
.slice(`${virtualIdPrefix}optional-webview:`.length)
|
|
2489
|
+
.replace(/\?.*$/, '')
|
|
2490
|
+
return id.endsWith('?missing')
|
|
2491
|
+
? 'export default undefined;\n'
|
|
2492
|
+
: `import { WebView } from ${JSON.stringify(pkg)};\nexport default WebView;\n`
|
|
2493
|
+
}
|
|
2494
|
+
if (id.startsWith(`${virtualIdPrefix}_expo_dom_entry`)) {
|
|
2495
|
+
const query = id.slice(`${virtualIdPrefix}_expo_dom_entry`.length)
|
|
2496
|
+
const params = new URLSearchParams(query)
|
|
2497
|
+
const fileUrl = params.get('file')
|
|
2498
|
+
if (!fileUrl) {
|
|
2499
|
+
throw new Error(`Missing file query param in DOM component entry: ${id}`)
|
|
2500
|
+
}
|
|
2501
|
+
let targetPath: string
|
|
2502
|
+
try {
|
|
2503
|
+
targetPath = fileUrl.startsWith('file:') ? fileURLToPath(fileUrl) : fileUrl
|
|
2504
|
+
} catch {
|
|
2505
|
+
targetPath = fileUrl
|
|
2506
|
+
}
|
|
2507
|
+
const internalWebPath = path.resolve(
|
|
2508
|
+
workspaceNodeModules,
|
|
2509
|
+
'expo/build/dom/internal/internal.web.js',
|
|
2510
|
+
)
|
|
2511
|
+
// bare specifiers on purpose: the workspace react/react-dom package
|
|
2512
|
+
// roots are CJS, and serving them by absolute path bypasses vite's
|
|
2513
|
+
// pre-bundling, so the browser gets raw CJS with no default export.
|
|
2514
|
+
// bare imports resolve through vite to the pre-bundled ESM build.
|
|
2515
|
+
return `import React from 'react';
|
|
2516
|
+
import * as ReactDOM from 'react-dom';
|
|
2517
|
+
import * as ReactDOMClient from 'react-dom/client';
|
|
2518
|
+
if (typeof window !== 'undefined') {
|
|
2519
|
+
window.React = React;
|
|
2520
|
+
window.ReactDOM = ReactDOM;
|
|
2521
|
+
window.ReactDOMClient = ReactDOMClient;
|
|
2522
|
+
window.require = function (mod) {
|
|
2523
|
+
if (mod === 'react') return React;
|
|
2524
|
+
if (mod === 'react-dom') return ReactDOM;
|
|
2525
|
+
if (mod === 'react-dom/client') return ReactDOMClient;
|
|
2526
|
+
if (mod === './withDevTools' || (typeof mod === 'string' && mod.endsWith('withDevTools'))) {
|
|
2527
|
+
return { withDevTools: function (c) { return c; } };
|
|
2528
|
+
}
|
|
2529
|
+
// expo's dom-entry calls require('@expo/log-box/lib').setupLogBox() in
|
|
2530
|
+
// dev; mirror the plugin's @expo/log-box virtual stub.
|
|
2531
|
+
if (mod === '@expo/log-box' || mod === '@expo/log-box/lib') {
|
|
2532
|
+
return { setupLogBox: function () {} };
|
|
2533
|
+
}
|
|
2534
|
+
// metro fast-refresh plumbing cannot run under vite; mirror the head
|
|
2535
|
+
// script's no-op shape for requires that execute after module eval.
|
|
2536
|
+
if (mod === 'react-refresh/runtime') {
|
|
2537
|
+
return {
|
|
2538
|
+
injectIntoGlobalHook: function () {},
|
|
2539
|
+
createSignatureFunctionForTransform: function () { return function (t) { return t; }; },
|
|
2540
|
+
isLikelyComponentType: function () { return false; },
|
|
2541
|
+
getFamilyByType: function () { return undefined; },
|
|
2542
|
+
register: function () {},
|
|
2543
|
+
performReactRefresh: function () {},
|
|
2544
|
+
hasUnrecoverableErrors: function () { return false; }
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
return {};
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
import { registerDOMComponent } from ${JSON.stringify(internalWebPath)};
|
|
2551
|
+
import Component from ${JSON.stringify(targetPath + '?dom-web')};
|
|
2552
|
+
registerDOMComponent(Component);
|
|
2553
|
+
`
|
|
2554
|
+
}
|
|
2555
|
+
return null
|
|
2556
|
+
},
|
|
2557
|
+
async transform(code, id) {
|
|
2558
|
+
const query = id.includes('?') ? id.slice(id.indexOf('?') + 1) : ''
|
|
2559
|
+
if (new URLSearchParams(query).has('dom-web')) {
|
|
2560
|
+
const cleanId = id.replace(/\?.*$/, '')
|
|
2561
|
+
const transformed = await transformWithOxc(code, cleanId, {
|
|
2562
|
+
lang: cleanId.endsWith('.tsx')
|
|
2563
|
+
? 'tsx'
|
|
2564
|
+
: cleanId.endsWith('.ts')
|
|
2565
|
+
? 'ts'
|
|
2566
|
+
: cleanId.endsWith('.jsx')
|
|
2567
|
+
? 'jsx'
|
|
2568
|
+
: 'js',
|
|
2569
|
+
jsx: { runtime: 'automatic' },
|
|
2570
|
+
})
|
|
2571
|
+
return { code: transformed.code, map: transformed.map ?? null }
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
const cleanId = id.replace(/\?.*$/, '')
|
|
2575
|
+
if (domWebModuleIds.has(cleanId) || domWebModuleIds.has(id)) {
|
|
2576
|
+
const rewritten = rewriteDomWebRequires(code)
|
|
2577
|
+
if (rewritten) return { code: rewritten, map: null }
|
|
2578
|
+
}
|
|
2579
|
+
// like expo's use-dom babel plugin, any module whose program opens with
|
|
2580
|
+
// the directive is a dom component, wherever it lives.
|
|
2581
|
+
if (!/\.(tsx?|jsx?)$/.test(cleanId)) return null
|
|
2582
|
+
if (!code.includes('use dom')) return null
|
|
2583
|
+
if (!/(?:^|\n)\s*['"]use dom['"](?:\s*;|\s*\n)/.test(code)) return null
|
|
2584
|
+
|
|
2585
|
+
const isTS = /\.(tsx?|mts|cts)$/.test(cleanId)
|
|
2586
|
+
const isTSX = cleanId.endsWith('.tsx')
|
|
2587
|
+
|
|
2588
|
+
const { transformAsync } = await getDomBabelCore()
|
|
2589
|
+
const { expoUseDomDirectivePlugin } = sootsimPluginRequire(
|
|
2590
|
+
'babel-preset-expo/build/plugins/use-dom-directive-plugin',
|
|
2591
|
+
)
|
|
2592
|
+
|
|
2593
|
+
const fileUrl = pathToFileURL(cleanId).href
|
|
2594
|
+
const hash = crypto.createHash('md5').update(fileUrl).digest('hex')
|
|
2595
|
+
domHashMap.set(hash, fileUrl)
|
|
2596
|
+
|
|
2597
|
+
const result = await transformAsync(code, {
|
|
2598
|
+
filename: cleanId,
|
|
2599
|
+
babelrc: false,
|
|
2600
|
+
configFile: false,
|
|
2601
|
+
sourceMaps: true,
|
|
2602
|
+
caller: {
|
|
2603
|
+
name: 'peachsim',
|
|
2604
|
+
platform: 'ios',
|
|
2605
|
+
isDev: !isProduction,
|
|
2606
|
+
projectRoot,
|
|
2607
|
+
},
|
|
2608
|
+
presets: isTS
|
|
2609
|
+
? [
|
|
2610
|
+
[
|
|
2611
|
+
sootsimPluginRequire.resolve('@babel/preset-typescript'),
|
|
2612
|
+
{ isTSX, allExtensions: isTSX, allowDeclareFields: true },
|
|
2613
|
+
],
|
|
2614
|
+
]
|
|
2615
|
+
: [],
|
|
2616
|
+
plugins: [
|
|
2617
|
+
[sootsimPluginRequire.resolve('@babel/plugin-syntax-jsx')],
|
|
2618
|
+
[expoUseDomDirectivePlugin],
|
|
2619
|
+
],
|
|
2620
|
+
})
|
|
2621
|
+
|
|
2622
|
+
if (result?.code) {
|
|
2623
|
+
return {
|
|
2624
|
+
code: result.code,
|
|
2625
|
+
map: result.map ?? null,
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
return null
|
|
2629
|
+
},
|
|
2630
|
+
}
|
|
2631
|
+
}
|