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.
Files changed (39) hide show
  1. package/README.md +165 -2
  2. package/package.json +52 -4
  3. package/react-native.config.js +24 -0
  4. package/src/bin/engine-path.js +12 -0
  5. package/src/cli/index.js +20 -0
  6. package/src/cli/init.js +112 -0
  7. package/src/cli/run.js +73 -0
  8. package/src/externaltexture/index.js +58 -0
  9. package/src/index.js +12 -0
  10. package/src/metro-shims/index.js +382 -0
  11. package/src/metro-shims/shims/Alert.impeller.js +89 -0
  12. package/src/metro-shims/shims/BaseViewConfig.impeller.js +81 -0
  13. package/src/metro-shims/shims/Button.impeller.js +100 -0
  14. package/src/metro-shims/shims/Image.impeller.js +97 -0
  15. package/src/metro-shims/shims/Platform.impeller-as-ios.js +44 -0
  16. package/src/metro-shims/shims/Platform.impeller.js +53 -0
  17. package/src/metro-shims/shims/ReactNativeFeatureFlags.impeller.js +40 -0
  18. package/src/metro-shims/shims/ReactNativeStyleAttributes.impeller.js +20 -0
  19. package/src/metro-shims/shims/TextInputState.impeller.js +100 -0
  20. package/src/metro-shims/shims/ViewConfigIgnore.impeller.js +55 -0
  21. package/src/metro-shims/shims/reanimated/clock.js +41 -0
  22. package/src/metro-shims/shims/reanimated/component.js +83 -0
  23. package/src/metro-shims/shims/reanimated/css.js +193 -0
  24. package/src/metro-shims/shims/reanimated/easing.js +47 -0
  25. package/src/metro-shims/shims/reanimated/hooks.js +191 -0
  26. package/src/metro-shims/shims/reanimated/interpolate.js +133 -0
  27. package/src/metro-shims/shims/reanimated/values.js +264 -0
  28. package/src/metro-shims/shims/reanimated.impeller.js +90 -0
  29. package/src/notifications/index.d.ts +44 -0
  30. package/src/notifications/index.js +95 -0
  31. package/src/platformview/index.js +23 -0
  32. package/src/resolve.js +238 -0
  33. package/src/selftest.js +123 -0
  34. package/src/storage/index.js +161 -0
  35. package/src/viewshot/index.js +98 -0
  36. package/src/worldraster/index.js +75 -0
  37. package/templates/app/README.md.tmpl +18 -0
  38. package/templates/app/_gitignore +1 -0
  39. 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;