react-native-impeller-platform 0.0.0-stage → 0.3.1
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 +165 -2
- package/package.json +52 -4
- package/react-native.config.js +24 -0
- package/src/bin/engine-path.js +12 -0
- package/src/cli/index.js +20 -0
- package/src/cli/init.js +112 -0
- package/src/cli/run.js +73 -0
- package/src/externaltexture/index.js +58 -0
- package/src/index.js +12 -0
- package/src/metro-shims/index.js +382 -0
- package/src/metro-shims/shims/Alert.impeller.js +89 -0
- package/src/metro-shims/shims/BaseViewConfig.impeller.js +81 -0
- package/src/metro-shims/shims/Button.impeller.js +100 -0
- package/src/metro-shims/shims/Image.impeller.js +97 -0
- package/src/metro-shims/shims/Platform.impeller-as-ios.js +44 -0
- package/src/metro-shims/shims/Platform.impeller.js +53 -0
- package/src/metro-shims/shims/ReactNativeFeatureFlags.impeller.js +40 -0
- package/src/metro-shims/shims/ReactNativeStyleAttributes.impeller.js +20 -0
- package/src/metro-shims/shims/TextInputState.impeller.js +100 -0
- package/src/metro-shims/shims/ViewConfigIgnore.impeller.js +55 -0
- package/src/metro-shims/shims/reanimated/clock.js +41 -0
- package/src/metro-shims/shims/reanimated/component.js +83 -0
- package/src/metro-shims/shims/reanimated/css.js +193 -0
- package/src/metro-shims/shims/reanimated/easing.js +47 -0
- package/src/metro-shims/shims/reanimated/hooks.js +191 -0
- package/src/metro-shims/shims/reanimated/interpolate.js +133 -0
- package/src/metro-shims/shims/reanimated/values.js +264 -0
- package/src/metro-shims/shims/reanimated.impeller.js +90 -0
- package/src/notifications/index.d.ts +44 -0
- package/src/notifications/index.js +95 -0
- package/src/platformview/index.js +23 -0
- package/src/resolve.js +238 -0
- package/src/selftest.js +123 -0
- package/src/storage/index.js +161 -0
- package/src/viewshot/index.js +98 -0
- package/src/worldraster/index.js +75 -0
- package/templates/app/README.md.tmpl +18 -0
- package/templates/app/_gitignore +1 -0
- package/templates/app/impeller.config.json.tmpl +11 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Metro integration for the `impeller` platform.
|
|
3
|
+
//
|
|
4
|
+
// PLAN D5/D6: `withImpeller()` WRAPS the consumer's metro config -- it chains an
|
|
5
|
+
// existing `resolveRequest` instead of replacing it, and it appends to
|
|
6
|
+
// `resolver.platforms` instead of overwriting. A metro plugin that clobbers those
|
|
7
|
+
// silently breaks every other RN plugin the app uses.
|
|
8
|
+
//
|
|
9
|
+
// Shims are resolved from `__dirname`, so linked / installed / tarball layouts all
|
|
10
|
+
// work identically. Never CWD-relative.
|
|
11
|
+
//
|
|
12
|
+
// WHY REDIRECT BY RESOLVED PATH, NOT BY MODULE NAME
|
|
13
|
+
// -------------------------------------------------
|
|
14
|
+
// React Native imports its own internals RELATIVELY: `Libraries/Utilities/Platform`
|
|
15
|
+
// is reached as `require('./Platform')` from inside react-native, so a resolver
|
|
16
|
+
// keyed on the string 'react-native/Libraries/Utilities/Platform' NEVER fires. The
|
|
17
|
+
// P1 consumer gate caught exactly that: the bundle built fine and contained no shim.
|
|
18
|
+
// So: let the upstream resolver do its job, then inspect the file it landed on and
|
|
19
|
+
// swap it. The `SHIM_TARGETS` suffixes below are matched against that resolved path.
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
const SHIM_DIR = path.join(__dirname, 'shims');
|
|
25
|
+
|
|
26
|
+
// resolved-path suffix (posix) -> shim file in SHIM_DIR
|
|
27
|
+
const SHIM_TARGETS = {
|
|
28
|
+
'react-native/Libraries/Utilities/Platform.js': 'Platform.impeller.js',
|
|
29
|
+
// P4. Alert.js is NOT a stub and resolves perfectly — it simply branches
|
|
30
|
+
// itself out of existence: `if (ios) {...} else if (android) {...}` with no
|
|
31
|
+
// else. On `impeller` the call never leaves JavaScript, so a flawless native
|
|
32
|
+
// AlertManager is unreachable and nothing anywhere reports a problem. See
|
|
33
|
+
// shims/Alert.impeller.js for the full note; this is a distinct failure from
|
|
34
|
+
// the self-resolving RCTAlertManager.js stub below, and BOTH had to be fixed.
|
|
35
|
+
'react-native/Libraries/Alert/Alert.js': 'Alert.impeller.js',
|
|
36
|
+
// desktop-input-gap P0 item 1. Bare BaseViewConfig.js is ALSO covered by
|
|
37
|
+
// the PLATFORM_FORKED ios-stub redirect below (it is a genuine ios-stub-
|
|
38
|
+
// disease file, unrelated to this entry), but SHIM_TARGETS is consulted
|
|
39
|
+
// FIRST in shimForResolvedPath -- see below -- so this entry wins and the
|
|
40
|
+
// PLATFORM_FORKED one is inert for this specifier. Kept that way
|
|
41
|
+
// deliberately (not removed from PLATFORM_FORKED) so it stays the ios-stub-
|
|
42
|
+
// disease REFERENCE case the P2 gate's mutation suite targets, and this
|
|
43
|
+
// shim is additive on top of it (see shims/BaseViewConfig.impeller.js for
|
|
44
|
+
// why): the ios-stub-disease fix is still exactly what runs underneath.
|
|
45
|
+
'react-native/Libraries/NativeComponent/BaseViewConfig.js':
|
|
46
|
+
'BaseViewConfig.impeller.js',
|
|
47
|
+
// desktop-input-gap item 6. RN core's ConditionallyIgnoredEventHandlers
|
|
48
|
+
// strips onClick/onPointerDown/onPointerUp/... from validAttributes unless
|
|
49
|
+
// Platform.OS === 'ios' -- see shims/ViewConfigIgnore.impeller.js for the
|
|
50
|
+
// full "native fires, JS never hears" story.
|
|
51
|
+
'react-native/Libraries/NativeComponent/ViewConfigIgnore.js':
|
|
52
|
+
'ViewConfigIgnore.impeller.js',
|
|
53
|
+
// parity item 7. TextInputState.js sends focus/blur commands only on ios/
|
|
54
|
+
// android, behind a Platform.OS check Metro inlines to 'impeller' at build
|
|
55
|
+
// time, so ref.focus()/ref.blur() never reached native. See the shim.
|
|
56
|
+
'react-native/Libraries/Components/TextInput/TextInputState.js':
|
|
57
|
+
'TextInputState.impeller.js',
|
|
58
|
+
// parity item 12. Image.js self re-exports to undefined on impeller; in a
|
|
59
|
+
// __DEV__ bundle that crashed LogBox on its first warning. SC-30: the shim
|
|
60
|
+
// now mounts the real RCTImageView host component. See the shim.
|
|
61
|
+
'react-native/Libraries/Image/Image.js': 'Image.impeller.js',
|
|
62
|
+
// SC-18. Adds a fontFamily processor (array -> CSS stack); see the shim.
|
|
63
|
+
'react-native/Libraries/Components/View/ReactNativeStyleAttributes.js':
|
|
64
|
+
'ReactNativeStyleAttributes.impeller.js',
|
|
65
|
+
// SC-24. Button branches on Platform.OS ios/android, both false here.
|
|
66
|
+
'react-native/Libraries/Components/Button.js': 'Button.impeller.js',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// PLATFORM-FORKED RN FILES ("ios-stub disease")
|
|
71
|
+
//
|
|
72
|
+
// Some react-native modules exist ONLY as `.ios.js` / `.android.js`, with a
|
|
73
|
+
// bare `Foo.js` whose entire body is `import Foo from './Foo'; export default
|
|
74
|
+
// Foo;` — a re-export that relies on the platform extension resolving to a
|
|
75
|
+
// sibling. On a custom platform there is no sibling, so `./Foo` resolves to the
|
|
76
|
+
// file ITSELF, the circular import yields `undefined`, and the export is
|
|
77
|
+
// silently undefined.
|
|
78
|
+
//
|
|
79
|
+
// The failure surfaces far away and blames the renderer. Measured here:
|
|
80
|
+
// BaseViewConfig.js self-resolved, so `PlatformBaseViewConfig` was undefined and
|
|
81
|
+
// ViewConfig.js died with `Cannot read property 'bubblingEventTypes' of
|
|
82
|
+
// undefined` while mounting the first <View> — which reads like a broken Fabric
|
|
83
|
+
// component registration and is nothing of the sort.
|
|
84
|
+
//
|
|
85
|
+
// Fix: redirect the bare file to the iOS flavour, which is the desktop-shaped
|
|
86
|
+
// one (the Android variants reach for JNI-backed view managers). This REDIRECTS
|
|
87
|
+
// TO RN'S OWN FILE rather than vendoring a copy, so an RN bump carries its own
|
|
88
|
+
// content and cannot go stale.
|
|
89
|
+
const PLATFORM_FORKED = {
|
|
90
|
+
'react-native/Libraries/NativeComponent/BaseViewConfig.js': 'BaseViewConfig.ios.js',
|
|
91
|
+
// P4. `Libraries/Alert/RCTAlertManager.js` is a textbook instance: its entire
|
|
92
|
+
// body is `export * from './RCTAlertManager'`, which on a platform with no
|
|
93
|
+
// sibling extension resolves to ITSELF. `alertWithArgs` is then undefined and
|
|
94
|
+
// `Alert.alert()` dies with "alertWithArgs is not a function" — pointing at
|
|
95
|
+
// our AlertManager TurboModule, which is fine.
|
|
96
|
+
//
|
|
97
|
+
// Note the shape of the trap: the NATIVE module can be perfect and the
|
|
98
|
+
// feature still 100% broken, because the JS never reaches it. That is why
|
|
99
|
+
// the P4 gate asserts a native-side side-effect counter (alerts_shown) and
|
|
100
|
+
// not just "the module is in the manifest".
|
|
101
|
+
'react-native/Libraries/Alert/RCTAlertManager.js': 'RCTAlertManager.ios.js',
|
|
102
|
+
// Same disease, same fix. Reached by fetch/XMLHttpRequest.
|
|
103
|
+
'react-native/Libraries/Network/RCTNetworking.js': 'RCTNetworking.ios.js',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// ENG-01a. FILES THAT BRANCH THEMSELVES OUT OF EXISTENCE ON A CUSTOM PLATFORM
|
|
108
|
+
//
|
|
109
|
+
// A third failure shape, distinct from both lists above. The file resolves
|
|
110
|
+
// perfectly, imports perfectly, and RUNS -- and then does nothing, because its
|
|
111
|
+
// entire body is `if (ios) {...} else if (android) {...}` with no else. Nothing
|
|
112
|
+
// throws and nothing logs. Alert.js was the first (fixed by vendoring a shim);
|
|
113
|
+
// TextInput.js is the second and CANNOT be vendored, because it is ~1000 lines
|
|
114
|
+
// of stateful RN internals we would then own forever across RN bumps.
|
|
115
|
+
//
|
|
116
|
+
// So instead of replacing the file, we change the ANSWER IT GETS. When
|
|
117
|
+
// TextInput.js asks `Platform.OS`, and only when TextInput.js asks, it is told
|
|
118
|
+
// 'ios' -- the desktop-shaped arm, same rule as PLATFORM_FORKED above. Every
|
|
119
|
+
// other importer of Platform still gets 'impeller'.
|
|
120
|
+
//
|
|
121
|
+
// ⚠️ MATCHED ON THE *IMPORTER*, NOT ON THE RESOLVED FILE.
|
|
122
|
+
// The other two tables inspect what a request resolved TO. This one inspects
|
|
123
|
+
// who ASKED. Metro gives us that as `context.originModulePath`. Keying this on
|
|
124
|
+
// the resolved path instead would swap Platform for the entire bundle, which
|
|
125
|
+
// is the global lie this design exists to avoid.
|
|
126
|
+
const IOS_BRANCHED_IMPORTERS = {
|
|
127
|
+
'react-native/Libraries/Components/TextInput/TextInput.js':
|
|
128
|
+
'Platform.impeller-as-ios.js',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// ENG-01c. MATCH ON THE ORIGIN *FOLDER*, NOT JUST THE FILE.
|
|
133
|
+
//
|
|
134
|
+
// The file table above is a lie in the presence of Metro's resolution cache.
|
|
135
|
+
// metro/src/node-haste/DependencyGraph.js resolveDependency() keys the cache on
|
|
136
|
+
// `(resolverOptions, originKey, targetKey, platform)` where, for a specifier
|
|
137
|
+
// that contains "/" (ours is `../../Utilities/Platform`), originKey is
|
|
138
|
+
// `path.dirname(originModulePath)` -- the origin FOLDER, not the file. It also
|
|
139
|
+
// splits targetKey by import kind (`\0esm` vs `\0cjs`). resolveRequest runs
|
|
140
|
+
// ONLY on a cache miss.
|
|
141
|
+
//
|
|
142
|
+
// Components/TextInput/ has three siblings importing ../../Utilities/Platform:
|
|
143
|
+
// TextInput.js (ESM import) -- must get as-ios or it mounts null
|
|
144
|
+
// InputAccessoryView.js (ESM import) -- shares TextInput.js's exact cache key
|
|
145
|
+
// TextInputState.js (CJS require) -- its own cache bucket
|
|
146
|
+
// Because TextInput.js and InputAccessoryView.js share one ESM cache entry for
|
|
147
|
+
// the whole folder, whichever resolves first fills it for both. When
|
|
148
|
+
// InputAccessoryView wins, TextInput.js is handed the cached plain-impeller
|
|
149
|
+
// Platform without resolveRequest ever running for it, branches itself out of
|
|
150
|
+
// existence, and mounts no native field. Build order decides the winner, which
|
|
151
|
+
// is why it flaked ~15% here and 6/6 in the HostApp app.
|
|
152
|
+
//
|
|
153
|
+
// FIX: key the origin rule on the FOLDER. Every importer in
|
|
154
|
+
// Components/TextInput/ gets as-ios. This is forced, not chosen -- the shared
|
|
155
|
+
// ESM cache key means TextInput.js and InputAccessoryView.js CANNOT be given
|
|
156
|
+
// different answers. It is also correct:
|
|
157
|
+
// - TextInputState.js only reads OS to pick focus/blur commands, and the
|
|
158
|
+
// iOS commands are exactly what the native field TextInput.js now mounts
|
|
159
|
+
// expects (previously, with plain impeller, focus/blur silently no-op'd).
|
|
160
|
+
// - InputAccessoryView.js is iOS-only API. as-ios makes it try to mount
|
|
161
|
+
// RCTInputAccessoryViewNativeComponent, which is NOT registered in
|
|
162
|
+
// FabricHost -- but that path only runs if an app actually mounts
|
|
163
|
+
// <InputAccessoryView>, the pre-fix code already leaked as-ios to it ~50%
|
|
164
|
+
// of the time with no reported breakage, and registering that component is
|
|
165
|
+
// a separate future ticket, not a regression introduced here.
|
|
166
|
+
const IOS_BRANCHED_ORIGIN_DIRS = {
|
|
167
|
+
'react-native/Libraries/Components/TextInput': 'Platform.impeller-as-ios.js',
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// The module specifier TextInput.js actually writes is RELATIVE
|
|
171
|
+
// (`import Platform from '../../Utilities/Platform'`), so this is checked
|
|
172
|
+
// against the RESOLVED Platform path, gated by the origin file.
|
|
173
|
+
const PLATFORM_MODULE_SUFFIX =
|
|
174
|
+
'react-native/Libraries/Utilities/Platform.js';
|
|
175
|
+
|
|
176
|
+
function toPosix(p) {
|
|
177
|
+
return p.split(path.sep).join('/');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function shimForOrigin(originModulePath, resolvedFilePath) {
|
|
181
|
+
if (!originModulePath || !resolvedFilePath) return null;
|
|
182
|
+
if (!toPosix(resolvedFilePath).endsWith(PLATFORM_MODULE_SUFFIX)) return null;
|
|
183
|
+
const origin = toPosix(originModulePath);
|
|
184
|
+
for (const [suffix, shim] of Object.entries(IOS_BRANCHED_IMPORTERS)) {
|
|
185
|
+
if (origin.endsWith(suffix)) return path.join(SHIM_DIR, shim);
|
|
186
|
+
}
|
|
187
|
+
// ENG-01c: folder-scoped fallback. Metro's resolution cache is keyed on the
|
|
188
|
+
// origin FOLDER, so a file-only rule loses the race for siblings that share
|
|
189
|
+
// the cache entry. Match the containing directory of the importer.
|
|
190
|
+
const originDir = toPosix(path.dirname(originModulePath));
|
|
191
|
+
for (const [dirSuffix, shim] of Object.entries(IOS_BRANCHED_ORIGIN_DIRS)) {
|
|
192
|
+
if (originDir.endsWith(dirSuffix)) return path.join(SHIM_DIR, shim);
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function shimForResolvedPath(filePath) {
|
|
198
|
+
if (!filePath) return null;
|
|
199
|
+
const posix = toPosix(filePath);
|
|
200
|
+
for (const [suffix, shim] of Object.entries(SHIM_TARGETS)) {
|
|
201
|
+
if (posix.endsWith(suffix)) return path.join(SHIM_DIR, shim);
|
|
202
|
+
}
|
|
203
|
+
for (const [suffix, sibling] of Object.entries(PLATFORM_FORKED)) {
|
|
204
|
+
if (posix.endsWith(suffix)) {
|
|
205
|
+
return path.join(path.dirname(filePath), sibling);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// parity item 20. Self re-export files redirected to their .ios.js sibling
|
|
212
|
+
// (see resolveRequest). Only files measured to break are listed; the others
|
|
213
|
+
// under Libraries/ are either already shimmed or render fine.
|
|
214
|
+
const SELF_REEXPORTS = [
|
|
215
|
+
'react-native/Libraries/StyleSheet/PlatformColorValueTypes.js',
|
|
216
|
+
'react-native/Libraries/Utilities/BackHandler.js',
|
|
217
|
+
'react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.js',
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
function selfReexportSibling(filePath) {
|
|
221
|
+
const posix = toPosix(filePath);
|
|
222
|
+
if (!SELF_REEXPORTS.some((rel) => posix.endsWith(rel))) return null;
|
|
223
|
+
const ios = filePath.replace(/\.js$/, '.ios.js');
|
|
224
|
+
return fs.existsSync(ios) ? ios : null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// parity item 20. See shims/ReactNativeFeatureFlags.impeller.js.
|
|
228
|
+
const FEATURE_FLAGS_SUFFIX =
|
|
229
|
+
'react-native/src/private/featureflags/ReactNativeFeatureFlags.js';
|
|
230
|
+
const FEATURE_FLAGS_SHIM = 'ReactNativeFeatureFlags.impeller.js';
|
|
231
|
+
|
|
232
|
+
// ENG-19. See resolveRequest and shims/reanimated.impeller.js.
|
|
233
|
+
const REANIMATED = 'react-native-reanimated';
|
|
234
|
+
const REANIMATED_SHIM = 'reanimated.impeller.js';
|
|
235
|
+
|
|
236
|
+
// EXPO. `expo start` (@expo/cli withMetroMultiPlatform) asks
|
|
237
|
+
// expo-modules-autolinking which package hosts react-native for the requested
|
|
238
|
+
// platform, on EVERY resolve, and that is a closed switch (ios/android ->
|
|
239
|
+
// react-native, tvos, macos, windows). For 'impeller' it THROWS "No support
|
|
240
|
+
// package is known for platform". The retry-as-ios below then caught that
|
|
241
|
+
// throw for every module, so `expo start` silently served an iOS bundle
|
|
242
|
+
// (Settings.ios.js, Platform.ios.js, Image.ios.js...) and the first ios-only
|
|
243
|
+
// TurboModule (SettingsManager) killed the dev boot. `react-native bundle`
|
|
244
|
+
// never calls it, which is why the same config bundled fine there.
|
|
245
|
+
//
|
|
246
|
+
// The impeller platform's host package IS react-native (this package renders
|
|
247
|
+
// stock react-native), so answer that, in the module @expo/cli reads, before
|
|
248
|
+
// it reads it. A project without expo is left alone.
|
|
249
|
+
function teachExpoImpellerPlatform(projectRoot) {
|
|
250
|
+
let platforms;
|
|
251
|
+
try {
|
|
252
|
+
const expoDir = path.dirname(require.resolve('expo/package.json', {paths: [projectRoot]}));
|
|
253
|
+
const exportsFile = require.resolve('expo-modules-autolinking/exports', {
|
|
254
|
+
paths: [path.join(expoDir, 'internal'), expoDir],
|
|
255
|
+
});
|
|
256
|
+
// The package's exports map points "./exports" at build/exports.js; older
|
|
257
|
+
// layouts had a root exports.js re-exporting ./build/exports.
|
|
258
|
+
const dir = path.dirname(exportsFile);
|
|
259
|
+
const candidates = [
|
|
260
|
+
path.join(dir, 'platforms', 'index.js'),
|
|
261
|
+
path.join(dir, 'build', 'platforms', 'index.js'),
|
|
262
|
+
];
|
|
263
|
+
const found = candidates.find((c) => fs.existsSync(c));
|
|
264
|
+
if (!found) return 'unknown-shape';
|
|
265
|
+
platforms = require(found);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return 'no-expo';
|
|
268
|
+
}
|
|
269
|
+
if (typeof platforms.getSupportPackageForPlatform !== 'function') return 'unknown-shape';
|
|
270
|
+
if (platforms.getSupportPackageForPlatform.__rniImpeller) return 'patched';
|
|
271
|
+
const original = platforms.getSupportPackageForPlatform;
|
|
272
|
+
const patched = function getSupportPackageForPlatform(platform) {
|
|
273
|
+
return platform === 'impeller' ? 'react-native' : original.apply(this, arguments);
|
|
274
|
+
};
|
|
275
|
+
patched.__rniImpeller = true;
|
|
276
|
+
platforms.getSupportPackageForPlatform = patched;
|
|
277
|
+
return 'patched';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function withImpeller(config = {}) {
|
|
281
|
+
teachExpoImpellerPlatform(config.projectRoot || process.cwd());
|
|
282
|
+
const resolver = config.resolver || {};
|
|
283
|
+
const existingPlatforms = resolver.platforms || ['ios', 'android', 'native'];
|
|
284
|
+
const platforms = existingPlatforms.includes('impeller')
|
|
285
|
+
? existingPlatforms
|
|
286
|
+
: [...existingPlatforms, 'impeller'];
|
|
287
|
+
|
|
288
|
+
const priorResolveRequest = resolver.resolveRequest;
|
|
289
|
+
|
|
290
|
+
function baseResolve(context, moduleName, platform) {
|
|
291
|
+
return priorResolveRequest
|
|
292
|
+
? priorResolveRequest(context, moduleName, platform)
|
|
293
|
+
: context.resolveRequest(context, moduleName, platform);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function resolveRequest(context, moduleName, platform) {
|
|
297
|
+
// ENG-19. react-native-reanimated: the engine vends no Reanimated/Worklets
|
|
298
|
+
// native module, and an app that does not install the package cannot even
|
|
299
|
+
// bundle (react-native-css requires it from native/reanimated.js). When
|
|
300
|
+
// the app has NOT installed it, resolve to the package's inert stub
|
|
301
|
+
// (shims/reanimated.impeller.js). An app that installs the real package
|
|
302
|
+
// keeps it: that is a deliberate choice, and hiding it would be a lie.
|
|
303
|
+
if (platform === 'impeller' && moduleName === REANIMATED) {
|
|
304
|
+
try {
|
|
305
|
+
return baseResolve(context, moduleName, platform);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
return {type: 'sourceFile', filePath: path.join(SHIM_DIR, REANIMATED_SHIM)};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
let resolution;
|
|
311
|
+
try {
|
|
312
|
+
resolution = baseResolve(context, moduleName, platform);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
// parity item 20. RN ships some modules ONLY as .ios.js + .android.js
|
|
315
|
+
// with no platform-neutral file, so resolving them for 'impeller'
|
|
316
|
+
// THROWS. ReactDevToolsSettingsManager (reached from setUpReactDevTools
|
|
317
|
+
// in every __DEV__ bundle) is one: no dev bundle could even be built.
|
|
318
|
+
// Retry as ios, and ONLY on a throw: a module that resolves to an ios
|
|
319
|
+
// stub exporting undefined is a different defect, and a blanket
|
|
320
|
+
// redirect would hide it. (Moved here from HostApp's metro.config.js.)
|
|
321
|
+
if (platform !== 'impeller') throw error;
|
|
322
|
+
// ⛔ NEVER retry a resolver that rejected the PLATFORM itself: that
|
|
323
|
+
// turns every module of the bundle into its iOS variant without a
|
|
324
|
+
// word (see teachExpoImpellerPlatform). Fail loudly instead.
|
|
325
|
+
if (/platform "impeller"/.test(String(error && error.message))) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
'react-native-impeller-platform: the bundler rejected platform "impeller" (' +
|
|
328
|
+
String(error.message).split('\n')[0] +
|
|
329
|
+
'). Refusing to fall back to ios for every module.',
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
resolution = baseResolve(context, moduleName, 'ios');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (platform !== 'impeller' || !resolution || resolution.type !== 'sourceFile') {
|
|
336
|
+
return resolution;
|
|
337
|
+
}
|
|
338
|
+
// parity item 20. Self re-export files: RN ships a few `Foo.js` whose body
|
|
339
|
+
// is `import Foo from './Foo'; export default Foo;`, meant to resolve
|
|
340
|
+
// through a .ios.js/.android.js sibling. On impeller there is no sibling,
|
|
341
|
+
// the import resolves back to ITSELF, and the default export is
|
|
342
|
+
// undefined. Measured in HostApp (Gluestack Popover): BackHandler ("Cannot
|
|
343
|
+
// read property 'addEventListener' of undefined") and
|
|
344
|
+
// legacySendAccessibilityEvent ("0,s.default is not a function").
|
|
345
|
+
const selfReexport = selfReexportSibling(resolution.filePath);
|
|
346
|
+
if (selfReexport) return {type: 'sourceFile', filePath: selfReexport};
|
|
347
|
+
// parity item 20. Feature-flags redirect (see the shim), except for the
|
|
348
|
+
// shim's own require of the real module.
|
|
349
|
+
const origin = toPosix(context.originModulePath || '');
|
|
350
|
+
if (
|
|
351
|
+
toPosix(resolution.filePath).endsWith(FEATURE_FLAGS_SUFFIX) &&
|
|
352
|
+
!origin.endsWith('/' + FEATURE_FLAGS_SHIM)
|
|
353
|
+
) {
|
|
354
|
+
return {type: 'sourceFile', filePath: path.join(SHIM_DIR, FEATURE_FLAGS_SHIM)};
|
|
355
|
+
}
|
|
356
|
+
// Origin-scoped first: it is the narrower rule, and the only one that can
|
|
357
|
+
// return a DIFFERENT Platform than the global shim.
|
|
358
|
+
const originShim = shimForOrigin(context.originModulePath, resolution.filePath);
|
|
359
|
+
if (originShim) return {type: 'sourceFile', filePath: originShim};
|
|
360
|
+
const shim = shimForResolvedPath(resolution.filePath);
|
|
361
|
+
// A shim that wraps the real module requires it by path; do not loop it back.
|
|
362
|
+
if (shim && origin === toPosix(shim)) return resolution;
|
|
363
|
+
return shim ? {type: 'sourceFile', filePath: shim} : resolution;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
...config,
|
|
368
|
+
resolver: {...resolver, platforms, resolveRequest},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = {
|
|
373
|
+
withImpeller,
|
|
374
|
+
teachExpoImpellerPlatform,
|
|
375
|
+
shimForResolvedPath,
|
|
376
|
+
shimForOrigin,
|
|
377
|
+
SHIM_TARGETS,
|
|
378
|
+
PLATFORM_FORKED,
|
|
379
|
+
IOS_BRANCHED_IMPORTERS,
|
|
380
|
+
IOS_BRANCHED_ORIGIN_DIRS,
|
|
381
|
+
SHIM_DIR,
|
|
382
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Alert shim for the `impeller` platform.
|
|
3
|
+
//
|
|
4
|
+
// ⛔ WHY THIS FILE EXISTS: RN's `Libraries/Alert/Alert.js` BRANCHES ITSELF OUT
|
|
5
|
+
// OF EXISTENCE on any platform that is not ios or android.
|
|
6
|
+
//
|
|
7
|
+
// static alert(...) {
|
|
8
|
+
// if (Platform.OS === 'ios') { ...prompt... }
|
|
9
|
+
// else if (Platform.OS === 'android') { ...DialogManager... }
|
|
10
|
+
// } // <-- no else. Silent no-op.
|
|
11
|
+
//
|
|
12
|
+
// There is no error, no warning, and no dead import to grep for. The
|
|
13
|
+
// AlertManager TurboModule can be perfect, registered, cached, and reachable
|
|
14
|
+
// from JS — and `Alert.alert()` still does absolutely nothing, because the
|
|
15
|
+
// call never leaves JavaScript.
|
|
16
|
+
//
|
|
17
|
+
// This is a DIFFERENT disease from the self-resolving `RCTAlertManager.js`
|
|
18
|
+
// re-export stub (handled by the PLATFORM_FORKED redirect in ../index.js).
|
|
19
|
+
// Both had to be fixed for Alert to work, and each one alone leaves the
|
|
20
|
+
// feature 100% broken with a completely healthy-looking app. It is also why
|
|
21
|
+
// the P4 gate asserts a NATIVE-side counter (`alerts_shown`) as well as a
|
|
22
|
+
// pixel: the pixel proves JS got its callback, the counter proves the call
|
|
23
|
+
// actually crossed the seam. A JS-side stub could satisfy either alone.
|
|
24
|
+
//
|
|
25
|
+
// We do NOT vendor a copy of Alert.js. This delegates to RN's own
|
|
26
|
+
// `RCTAlertManager` (which our resolver points at the .ios flavour), so RN
|
|
27
|
+
// keeps ownership of the payload shape and an RN bump carries its own changes.
|
|
28
|
+
|
|
29
|
+
const {alertWithArgs} = require('react-native/Libraries/Alert/RCTAlertManager');
|
|
30
|
+
|
|
31
|
+
class Alert {
|
|
32
|
+
static alert(title, message, buttons, options) {
|
|
33
|
+
Alert.prompt(title, message, buttons, 'default', undefined, undefined, options);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static prompt(title, message, callbackOrButtons, type, defaultValue, keyboardType, options) {
|
|
37
|
+
// Same normalisation RN's ios branch does. Kept here rather than
|
|
38
|
+
// reimplemented differently, so button indices line up with the callback
|
|
39
|
+
// ids the native side sends back.
|
|
40
|
+
let callbacks = [];
|
|
41
|
+
const buttonSpecs = [];
|
|
42
|
+
let cancelButtonKey;
|
|
43
|
+
let destructiveButtonKey;
|
|
44
|
+
let preferredButtonKey;
|
|
45
|
+
|
|
46
|
+
if (typeof callbackOrButtons === 'function') {
|
|
47
|
+
callbacks = [callbackOrButtons];
|
|
48
|
+
} else if (Array.isArray(callbackOrButtons)) {
|
|
49
|
+
callbackOrButtons.forEach((btn, index) => {
|
|
50
|
+
callbacks[index] = btn.onPress;
|
|
51
|
+
if (btn.style === 'cancel') {
|
|
52
|
+
cancelButtonKey = String(index);
|
|
53
|
+
} else if (btn.style === 'destructive') {
|
|
54
|
+
destructiveButtonKey = String(index);
|
|
55
|
+
}
|
|
56
|
+
if (btn.isPreferred) {
|
|
57
|
+
preferredButtonKey = String(index);
|
|
58
|
+
}
|
|
59
|
+
if (btn.text || index < (callbackOrButtons || []).length - 1) {
|
|
60
|
+
const btnDef = {};
|
|
61
|
+
btnDef[index] = btn.text || '';
|
|
62
|
+
buttonSpecs.push(btnDef);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
alertWithArgs(
|
|
68
|
+
{
|
|
69
|
+
title: title || '',
|
|
70
|
+
message: message || undefined,
|
|
71
|
+
buttons: buttonSpecs,
|
|
72
|
+
type: type || undefined,
|
|
73
|
+
defaultValue,
|
|
74
|
+
cancelButtonKey,
|
|
75
|
+
destructiveButtonKey,
|
|
76
|
+
preferredButtonKey,
|
|
77
|
+
keyboardType,
|
|
78
|
+
userInterfaceStyle: (options && options.userInterfaceStyle) || undefined,
|
|
79
|
+
},
|
|
80
|
+
(id, value) => {
|
|
81
|
+
const cb = callbacks[id];
|
|
82
|
+
cb && cb(value);
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = Alert;
|
|
89
|
+
module.exports.default = Alert;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// desktop-input-gap P0 item 1. onKeyDown/onKeyUp view-config extension for
|
|
3
|
+
// the `impeller` platform.
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS EXISTS: BaseViewConfig.ios.js (what the PLATFORM_FORKED redirect
|
|
6
|
+
// in ../index.js used to point bare BaseViewConfig.js at, before this file
|
|
7
|
+
// existed) declares NEITHER bubblingEventTypes.topKeyDown/topKeyUp NOR
|
|
8
|
+
// validAttributes.onKeyDown/onKeyUp -- RN 0.86's C++ BaseViewEventEmitter has
|
|
9
|
+
// no onKeyDown/onKeyUp method at all (see docs/eng/desktop-input-gap.md), and
|
|
10
|
+
// the JS-side view config for a plain <View> never declared the matching
|
|
11
|
+
// bubbling registration either. So even though FabricHost.cpp's
|
|
12
|
+
// dispatchKeyEvent dispatches a generic 'keyDown'/'keyUp' RawEvent (which
|
|
13
|
+
// EventEmitter::normalizeEventType turns into exactly 'topKeyDown'/
|
|
14
|
+
// 'topKeyUp'), nothing in the view config told Fabric's event system that
|
|
15
|
+
// name maps to an onKeyDown/onKeyUp prop -- the native side would be fully
|
|
16
|
+
// wired and JS would never hear it. Same "native perfect, JS never told"
|
|
17
|
+
// shape Alert.js's self-branching produced (see Alert.impeller.js), one
|
|
18
|
+
// layer further down the stack.
|
|
19
|
+
//
|
|
20
|
+
// We do NOT vendor a copy of BaseViewConfig.ios.js: this delegates to RN's
|
|
21
|
+
// own file (requested with its EXPLICIT .ios.js extension so metro resolves
|
|
22
|
+
// it directly, bypassing the platform-redirect machinery this same module
|
|
23
|
+
// implements -- requesting the bare 'BaseViewConfig.js' specifier here would
|
|
24
|
+
// recurse into this exact shim) and ADDS the two event names, so an RN bump
|
|
25
|
+
// carries the bulk of the content and only the keys named here are ours to
|
|
26
|
+
// maintain.
|
|
27
|
+
const BaseViewConfigIosModule = require('react-native/Libraries/NativeComponent/BaseViewConfig.ios.js');
|
|
28
|
+
const BaseViewConfigIos = BaseViewConfigIosModule.default || BaseViewConfigIosModule;
|
|
29
|
+
|
|
30
|
+
const BaseViewConfigImpeller = {
|
|
31
|
+
...BaseViewConfigIos,
|
|
32
|
+
bubblingEventTypes: {
|
|
33
|
+
...BaseViewConfigIos.bubblingEventTypes,
|
|
34
|
+
// Matches the shape every other bubbling entry in BaseViewConfig.ios.js
|
|
35
|
+
// uses (topPress -> onPress/onPressCapture, etc.) and the react-native-
|
|
36
|
+
// windows/react-native-macos onKeyDown/onKeyUp payload fields (key, code,
|
|
37
|
+
// altKey, ctrlKey, metaKey, shiftKey) FabricHost::dispatchKeyEvent sends.
|
|
38
|
+
topKeyDown: {
|
|
39
|
+
phasedRegistrationNames: {
|
|
40
|
+
bubbled: 'onKeyDown',
|
|
41
|
+
captured: 'onKeyDownCapture',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
topKeyUp: {
|
|
45
|
+
phasedRegistrationNames: {
|
|
46
|
+
bubbled: 'onKeyUp',
|
|
47
|
+
captured: 'onKeyUpCapture',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
// desktop-input-gap item 6. Release of a non-left button over the view
|
|
51
|
+
// it was pressed on (react-native-macos/-windows `onAuxClick`; payload is
|
|
52
|
+
// a PointerEvent with button 1 middle, 2 right, 3 back, 4 forward).
|
|
53
|
+
topAuxClick: {
|
|
54
|
+
phasedRegistrationNames: {
|
|
55
|
+
bubbled: 'onAuxClick',
|
|
56
|
+
captured: 'onAuxClickCapture',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
validAttributes: {
|
|
61
|
+
...BaseViewConfigIos.validAttributes,
|
|
62
|
+
onKeyDown: true,
|
|
63
|
+
onKeyDownCapture: true,
|
|
64
|
+
onKeyUp: true,
|
|
65
|
+
onKeyUpCapture: true,
|
|
66
|
+
onAuxClick: true,
|
|
67
|
+
onAuxClickCapture: true,
|
|
68
|
+
// desktop-input-gap item 11. A Tab-focused <View focusable> receives
|
|
69
|
+
// onFocus/onBlur (FabricHost dispatches 'focus'/'blur', normalized to the
|
|
70
|
+
// topFocus/topBlur bubbling types BaseViewConfig.ios.js already declares).
|
|
71
|
+
// iOS declares the event types but not these props for View, so without
|
|
72
|
+
// them React would never keep a View's handler.
|
|
73
|
+
onFocus: true,
|
|
74
|
+
onFocusCapture: true,
|
|
75
|
+
onBlur: true,
|
|
76
|
+
onBlurCapture: true,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
module.exports = BaseViewConfigImpeller;
|
|
81
|
+
module.exports.default = BaseViewConfigImpeller;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Button shim for the `impeller` platform (SC-24).
|
|
3
|
+
//
|
|
4
|
+
// RN's Button.js picks its look with Platform.OS / Platform.select, which Metro
|
|
5
|
+
// inlines to 'impeller' at build time: the android branch is not taken (no
|
|
6
|
+
// TouchableNativeFeedback) but neither is the ios one, so `styles.button` is
|
|
7
|
+
// undefined and a `color` prop becomes a filled backgroundColor block with the
|
|
8
|
+
// default black title. This copy hard-wires the iOS look: blue (or `color`)
|
|
9
|
+
// title, no filled block, TouchableOpacity.
|
|
10
|
+
|
|
11
|
+
const React = require('react');
|
|
12
|
+
const interop = (m) => (m && m.default) || m;
|
|
13
|
+
const StyleSheet = interop(require('react-native/Libraries/StyleSheet/StyleSheet'));
|
|
14
|
+
const Text = interop(require('react-native/Libraries/Text/Text'));
|
|
15
|
+
const TouchableOpacity = interop(require('react-native/Libraries/Components/Touchable/TouchableOpacity'));
|
|
16
|
+
const View = interop(require('react-native/Libraries/Components/View/View'));
|
|
17
|
+
|
|
18
|
+
const Button = ({ref, ...props}) => {
|
|
19
|
+
const {
|
|
20
|
+
accessibilityLabel,
|
|
21
|
+
accessibilityState,
|
|
22
|
+
'aria-busy': ariaBusy,
|
|
23
|
+
'aria-checked': ariaChecked,
|
|
24
|
+
'aria-disabled': ariaDisabled,
|
|
25
|
+
'aria-expanded': ariaExpanded,
|
|
26
|
+
'aria-label': ariaLabel,
|
|
27
|
+
'aria-selected': ariaSelected,
|
|
28
|
+
accessible,
|
|
29
|
+
accessibilityActions,
|
|
30
|
+
accessibilityHint,
|
|
31
|
+
accessibilityLanguage,
|
|
32
|
+
onAccessibilityAction,
|
|
33
|
+
importantForAccessibility,
|
|
34
|
+
color,
|
|
35
|
+
disabled: disabledProp,
|
|
36
|
+
onPress,
|
|
37
|
+
testID,
|
|
38
|
+
title,
|
|
39
|
+
touchSoundDisabled,
|
|
40
|
+
} = props;
|
|
41
|
+
const textStyles = [styles.text];
|
|
42
|
+
if (color) {
|
|
43
|
+
textStyles.push({color});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let state = {
|
|
47
|
+
busy: ariaBusy ?? accessibilityState?.busy,
|
|
48
|
+
checked: ariaChecked ?? accessibilityState?.checked,
|
|
49
|
+
disabled: ariaDisabled ?? accessibilityState?.disabled,
|
|
50
|
+
expanded: ariaExpanded ?? accessibilityState?.expanded,
|
|
51
|
+
selected: ariaSelected ?? accessibilityState?.selected,
|
|
52
|
+
};
|
|
53
|
+
const disabled = disabledProp != null ? disabledProp : state.disabled;
|
|
54
|
+
if (disabled !== state.disabled) {
|
|
55
|
+
state = {...state, disabled};
|
|
56
|
+
}
|
|
57
|
+
if (disabled) {
|
|
58
|
+
textStyles.push(styles.textDisabled);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof title !== 'string') {
|
|
62
|
+
throw new Error('The title prop of a Button must be a string');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return React.createElement(
|
|
66
|
+
TouchableOpacity,
|
|
67
|
+
{
|
|
68
|
+
accessible,
|
|
69
|
+
accessibilityActions,
|
|
70
|
+
onAccessibilityAction,
|
|
71
|
+
accessibilityLabel: ariaLabel || accessibilityLabel,
|
|
72
|
+
accessibilityHint,
|
|
73
|
+
accessibilityLanguage,
|
|
74
|
+
accessibilityRole: 'button',
|
|
75
|
+
accessibilityState: state,
|
|
76
|
+
importantForAccessibility:
|
|
77
|
+
importantForAccessibility === 'no' ? 'no-hide-descendants' : importantForAccessibility,
|
|
78
|
+
testID,
|
|
79
|
+
disabled,
|
|
80
|
+
onPress,
|
|
81
|
+
touchSoundDisabled,
|
|
82
|
+
ref,
|
|
83
|
+
},
|
|
84
|
+
React.createElement(
|
|
85
|
+
View,
|
|
86
|
+
null,
|
|
87
|
+
React.createElement(Text, {style: textStyles, disabled}, title),
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
Button.displayName = 'Button';
|
|
93
|
+
|
|
94
|
+
const styles = StyleSheet.create({
|
|
95
|
+
text: {textAlign: 'center', margin: 8, color: '#007AFF', fontSize: 18},
|
|
96
|
+
textDisabled: {color: '#cdcdcd'},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
module.exports = Button;
|
|
100
|
+
module.exports.default = Button;
|