react-native-impeller-platform 0.0.0-bootstrap.0 → 0.1.0

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 (37) hide show
  1. package/README.md +164 -1
  2. package/package.json +47 -3
  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/platformview/index.js +23 -0
  30. package/src/resolve.js +238 -0
  31. package/src/selftest.js +123 -0
  32. package/src/storage/index.js +161 -0
  33. package/src/viewshot/index.js +98 -0
  34. package/src/worldraster/index.js +75 -0
  35. package/templates/app/README.md.tmpl +18 -0
  36. package/templates/app/_gitignore +1 -0
  37. package/templates/app/impeller.config.json.tmpl +11 -0
@@ -0,0 +1,264 @@
1
+ 'use strict';
2
+ // SC-26. Shared values and the with* animations of the reanimated stub, run
3
+ // on the JS thread by clock.js. An animation is a plain object with
4
+ // start(from, now) and frame(now) -> done; assigning one to sv.value runs it.
5
+ const {now, onFrame} = require('./clock');
6
+ const {Easing, toEasing} = require('./easing');
7
+ const {mix} = require('./interpolate');
8
+
9
+ const ANIM = Symbol.for('rni.reanimated.animation');
10
+ const isAnimation = (v) => v != null && typeof v === 'object' && v[ANIM] === true;
11
+
12
+ // Shared values read while a tracker is set are recorded (useAnimatedStyle /
13
+ // useDerivedValue re-run when one of them changes).
14
+ let tracker = null;
15
+ function track(fn) {
16
+ const prev = tracker;
17
+ const seen = new Set();
18
+ tracker = seen;
19
+ try {
20
+ return {result: fn(), deps: seen};
21
+ } finally {
22
+ tracker = prev;
23
+ }
24
+ }
25
+
26
+ class SharedValue {
27
+ constructor(v) {
28
+ this._value = v;
29
+ this._anim = null;
30
+ this._listeners = new Map();
31
+ this._seq = 0;
32
+ }
33
+ get value() {
34
+ if (tracker) tracker.add(this);
35
+ return this._value;
36
+ }
37
+ set value(v) {
38
+ this._anim = null;
39
+ if (isAnimation(v)) {
40
+ const anim = v;
41
+ this._anim = anim;
42
+ anim.start(this._value, now());
43
+ onFrame((t) => {
44
+ if (this._anim !== anim) return false;
45
+ const done = anim.frame(t);
46
+ this._write(anim.current);
47
+ if (done) {
48
+ this._anim = null;
49
+ anim.finish(true);
50
+ }
51
+ return !done;
52
+ });
53
+ return;
54
+ }
55
+ this._write(v);
56
+ }
57
+ _write(v) {
58
+ this._value = v;
59
+ for (const fn of Array.from(this._listeners.values())) fn(v);
60
+ }
61
+ get() {
62
+ return this.value;
63
+ }
64
+ set(v) {
65
+ this.value = typeof v === 'function' ? v(this._value) : v;
66
+ }
67
+ modify(fn) {
68
+ if (fn) this.value = fn(this._value);
69
+ }
70
+ addListener(id, fn) {
71
+ this._listeners.set(id, fn);
72
+ }
73
+ removeListener(id) {
74
+ this._listeners.delete(id);
75
+ }
76
+ // internal: subscribe with an auto id
77
+ _subscribe(fn) {
78
+ const id = `__rni${this._seq++}`;
79
+ this._listeners.set(id, fn);
80
+ return () => this._listeners.delete(id);
81
+ }
82
+ _cancel() {
83
+ const a = this._anim;
84
+ this._anim = null;
85
+ if (a) a.finish(false);
86
+ }
87
+ }
88
+
89
+ const makeMutable = (v) => new SharedValue(v);
90
+ const cancelAnimation = (sv) => sv && sv._cancel && sv._cancel();
91
+
92
+ function animation(spec, callback) {
93
+ // defineProperties, not spread: withDelay forwards toValue through accessors.
94
+ const a = {
95
+ [ANIM]: true,
96
+ current: undefined,
97
+ finish(ok) {
98
+ if (callback) callback(ok, this.current);
99
+ },
100
+ };
101
+ return Object.defineProperties(a, Object.getOwnPropertyDescriptors(spec));
102
+ }
103
+
104
+ function withTiming(toValue, config, callback) {
105
+ const duration = config && config.duration != null ? config.duration : 300;
106
+ const ease = toEasing(config && config.easing, Easing.inOut(Easing.quad));
107
+ return animation({
108
+ toValue,
109
+ start(from, t) {
110
+ this.from = from;
111
+ this.t0 = t;
112
+ this.current = from;
113
+ },
114
+ frame(t) {
115
+ const p = duration <= 0 ? 1 : Math.min(1, (t - this.t0) / duration);
116
+ this.current = p >= 1 ? this.toValue : mix(this.from, this.toValue, ease(p));
117
+ return p >= 1;
118
+ },
119
+ }, callback);
120
+ }
121
+
122
+ // Damped harmonic oscillator, Reanimated 3's defaults, integrated in 1 ms
123
+ // steps so the result does not depend on the frame rate.
124
+ function withSpring(toValue, config, callback) {
125
+ const c = {mass: 1, stiffness: 100, damping: 10, velocity: 0, overshootClamping: false,
126
+ restDisplacementThreshold: 0.01, restSpeedThreshold: 2, ...(config || {})};
127
+ return animation({
128
+ toValue,
129
+ start(from, t) {
130
+ this.x = typeof from === 'number' ? from : toValue;
131
+ this.v = c.velocity;
132
+ this.t = t;
133
+ this.current = this.x;
134
+ },
135
+ frame(t) {
136
+ const to = this.toValue;
137
+ let steps = Math.min(Math.max(0, Math.round(t - this.t)), 1000);
138
+ this.t = t;
139
+ while (steps-- > 0) {
140
+ const a = (-c.stiffness * (this.x - to) - c.damping * this.v) / c.mass;
141
+ this.v += a * 0.001;
142
+ this.x += this.v * 0.001;
143
+ }
144
+ const overshot = c.overshootClamping && (this.x - to) * (this.current - to) < 0;
145
+ const rest = Math.abs(this.x - to) < c.restDisplacementThreshold && Math.abs(this.v) < c.restSpeedThreshold;
146
+ this.current = overshot || rest ? to : this.x;
147
+ return overshot || rest;
148
+ },
149
+ }, callback);
150
+ }
151
+
152
+ function withDecay(config, callback) {
153
+ const c = {velocity: 0, deceleration: 0.998, ...(config || {})};
154
+ return animation({
155
+ start(from, t) {
156
+ this.x = typeof from === 'number' ? from : 0;
157
+ this.v = c.velocity;
158
+ this.t = t;
159
+ this.current = this.x;
160
+ },
161
+ frame(t) {
162
+ const dt = Math.max(0, t - this.t);
163
+ this.t = t;
164
+ const k = Math.pow(c.deceleration, dt);
165
+ this.x += (this.v / 1000) * ((1 - k) / (1 - c.deceleration));
166
+ this.v *= k;
167
+ let done = Math.abs(this.v) < 0.5;
168
+ if (Array.isArray(c.clamp)) {
169
+ const [lo, hi] = c.clamp;
170
+ if (this.x <= lo || this.x >= hi) {
171
+ this.x = Math.min(Math.max(this.x, lo), hi);
172
+ done = true;
173
+ }
174
+ }
175
+ this.current = this.x;
176
+ return done;
177
+ },
178
+ }, callback);
179
+ }
180
+
181
+ const toAnim = (a) => (isAnimation(a) ? a : withTiming(a, {duration: 0}));
182
+
183
+ function withDelay(delayMs, inner) {
184
+ const a = toAnim(inner);
185
+ return animation({
186
+ get toValue() {
187
+ return a.toValue;
188
+ },
189
+ set toValue(v) {
190
+ a.toValue = v;
191
+ },
192
+ start(from, t) {
193
+ this.t0 = t;
194
+ this.from = from;
195
+ this.started = false;
196
+ this.current = from;
197
+ },
198
+ frame(t) {
199
+ if (t - this.t0 < delayMs) return false;
200
+ if (!this.started) {
201
+ this.started = true;
202
+ a.start(this.from, this.t0 + delayMs);
203
+ }
204
+ const done = a.frame(t);
205
+ this.current = a.current;
206
+ if (done) a.finish(true);
207
+ return done;
208
+ },
209
+ });
210
+ }
211
+
212
+ function withSequence(...inners) {
213
+ const list = inners.map(toAnim);
214
+ return animation({
215
+ start(from, t) {
216
+ this.i = 0;
217
+ this.current = from;
218
+ if (list.length) list[0].start(from, t);
219
+ },
220
+ frame(t) {
221
+ while (this.i < list.length) {
222
+ const a = list[this.i];
223
+ const done = a.frame(t);
224
+ this.current = a.current;
225
+ if (!done) return false;
226
+ a.finish(true);
227
+ this.i++;
228
+ if (this.i < list.length) list[this.i].start(this.current, t);
229
+ }
230
+ return true;
231
+ },
232
+ });
233
+ }
234
+
235
+ // numberOfReps <= 0 repeats forever; reverse swaps the inner animation's
236
+ // target with its start value on every other run.
237
+ function withRepeat(inner, numberOfReps = 2, reverse = false, callback) {
238
+ const a = toAnim(inner);
239
+ return animation({
240
+ start(from, t) {
241
+ this.n = 0;
242
+ this.origin = from;
243
+ this.target = a.toValue;
244
+ this.current = from;
245
+ a.start(from, t);
246
+ },
247
+ frame(t) {
248
+ const done = a.frame(t);
249
+ this.current = a.current;
250
+ if (!done) return false;
251
+ a.finish(true);
252
+ this.n++;
253
+ if (numberOfReps > 0 && this.n >= numberOfReps) return true;
254
+ if (reverse && 'toValue' in a) a.toValue = this.n % 2 ? this.origin : this.target;
255
+ a.start(reverse ? this.current : this.origin, t);
256
+ return false;
257
+ },
258
+ }, callback);
259
+ }
260
+
261
+ module.exports = {
262
+ SharedValue, makeMutable, isAnimation, track, cancelAnimation,
263
+ withTiming, withSpring, withDecay, withDelay, withSequence, withRepeat,
264
+ };
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+ // ENG-19 (moved from HostApp's app-level metro-shims/reanimated-stub.js).
3
+ // react-native-reanimated for the `impeller` platform.
4
+ //
5
+ // WHY: the engine vends no Reanimated / Worklets native module, so the real
6
+ // package cannot initialise, and an app that does not install it cannot even
7
+ // BUNDLE: react-native-css (NativeWind 5) require()s 'react-native-reanimated'
8
+ // from native/reanimated.js on every className render that carries a
9
+ // transition/animation, and Gluestack's motion parts import it at module top.
10
+ //
11
+ // WHAT IT DOES (SC-26): animated components are wrappers that re-render on a
12
+ // JS-thread clock, shared values and with* helpers really animate, and
13
+ // react-native-css transitions / animate-* move. Layout-animation presets stay
14
+ // chainable no-ops. Resolved only when the app has NOT
15
+ // installed react-native-reanimated itself (see metro-shims/index.js).
16
+ const RN = require('react-native');
17
+ const {createAnimatedComponent: animated} = require('./reanimated/component');
18
+ const hooks = require('./reanimated/hooks');
19
+ const values = require('./reanimated/values');
20
+ const {Easing: StubEasing, cubicBezier, cssSteps} = require('./reanimated/easing');
21
+ const {interpolate: interp} = require('./reanimated/interpolate');
22
+
23
+ // Chainable layout-animation builder: FadeIn.duration(200).delay(50) etc. all
24
+ // return the same inert object.
25
+ const preset = new Proxy(function () {}, {
26
+ get: (t, k) => (k === '__esModule' ? false : k === 'build' ? () => () => ({}) : () => preset),
27
+ apply: () => preset,
28
+ });
29
+
30
+ const Animated = {
31
+ View: animated(RN.View),
32
+ Text: animated(RN.Text),
33
+ Image: animated(RN.Image),
34
+ ScrollView: animated(RN.ScrollView),
35
+ FlatList: animated(RN.FlatList),
36
+ createAnimatedComponent: animated,
37
+ };
38
+
39
+ const sharedValue = (v) => ({value: v, get: () => v, set: () => {}, modify: () => {}});
40
+
41
+ module.exports = {
42
+ __esModule: true,
43
+ __impellerStub: true,
44
+ default: Animated,
45
+ createAnimatedComponent: animated,
46
+ // layout-animation presets Gluestack's parts reference
47
+ FadeIn: preset,
48
+ FadeOut: preset,
49
+ FadeInUp: preset,
50
+ FadeInDown: preset,
51
+ FadeOutUp: preset,
52
+ FadeOutDown: preset,
53
+ ZoomIn: preset,
54
+ ZoomOut: preset,
55
+ SlideInDown: preset,
56
+ SlideOutDown: preset,
57
+ SlideInUp: preset,
58
+ SlideOutUp: preset,
59
+ SlideInLeft: preset,
60
+ SlideOutLeft: preset,
61
+ SlideInRight: preset,
62
+ SlideOutRight: preset,
63
+ Layout: preset,
64
+ LinearTransition: preset,
65
+ // hooks and animation helpers (JS-thread implementation, see ./reanimated/)
66
+ useSharedValue: hooks.useSharedValue,
67
+ useDerivedValue: hooks.useDerivedValue,
68
+ useAnimatedStyle: hooks.useAnimatedStyle,
69
+ useAnimatedProps: hooks.useAnimatedProps,
70
+ useAnimatedRef: () => ({current: null}),
71
+ useAnimatedReaction: hooks.useAnimatedReaction,
72
+ useAnimatedScrollHandler: hooks.useAnimatedScrollHandler,
73
+ useReducedMotion: () => false,
74
+ withTiming: values.withTiming,
75
+ withSpring: values.withSpring,
76
+ withDecay: values.withDecay,
77
+ withDelay: values.withDelay,
78
+ withRepeat: values.withRepeat,
79
+ withSequence: values.withSequence,
80
+ cancelAnimation: values.cancelAnimation,
81
+ runOnJS: (f) => f,
82
+ runOnUI: (f) => f,
83
+ interpolate: interp,
84
+ Extrapolation: {CLAMP: 'clamp', EXTEND: 'extend', IDENTITY: 'identity'},
85
+ ReduceMotion: {System: 'system', Always: 'always', Never: 'never'},
86
+ Easing: StubEasing,
87
+ // CSS-animation timing helpers react-native-css reads
88
+ cubicBezier,
89
+ steps: cssSteps,
90
+ };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+ // FP-44 — platform views (Flutter's PlatformViewLayer + ExternalViewEmbedder).
3
+ //
4
+ // <ImpellerPlatformView color="#00aa00" style={...} /> is a native OS child view
5
+ // (an X11 child window on linux windowed GL) that the engine positions, clips
6
+ // and stacks to the view's box every frame; RN content drawn after it in paint
7
+ // order is composited above it. This first native view is a solid colour.
8
+ // Translate and rect clips are composited; scale/rotate/skew are not (the view
9
+ // is placed at the transformed bounds and the engine logs
10
+ // `platform_view_unsupported`). Hosts without an embedder (darwin, Windows,
11
+ // headless) paint the colour in place.
12
+
13
+ const {processColor} = require('react-native');
14
+ const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');
15
+
16
+ const ImpellerPlatformView = NativeComponentRegistry.get('ImpellerPlatformView', () => ({
17
+ uiViewClassName: 'ImpellerPlatformView',
18
+ bubblingEventTypes: {},
19
+ directEventTypes: {},
20
+ validAttributes: {color: {process: processColor}},
21
+ }));
22
+
23
+ module.exports = {ImpellerPlatformView};
package/src/resolve.js ADDED
@@ -0,0 +1,238 @@
1
+ 'use strict';
2
+ // Locates the prebuilt Impeller engine binary for the current platform.
3
+ //
4
+ // Delivery model (PLAN D4): the main package declares every
5
+ // `@blbaker/react-native-impeller-platform-engine-<platform>-<arch>` as an OPTIONAL dependency.
6
+ // npm installs only the one matching the host, so a consumer never compiles C++.
7
+ //
8
+ // Rules this file enforces:
9
+ // 1. `IMPELLER_ENGINE_BINARY` env var wins (local engine development).
10
+ // 2. Otherwise resolve the per-platform package from THIS file's location
11
+ // (`__dirname`) FIRST -- install and tarball must both work, and an
12
+ // installed copy must never be outranked by something in the CWD.
13
+ // 3. On failure, throw an error that NAMES the missing package. A resolver that
14
+ // says "not found" without the package name costs the consumer an hour.
15
+ // 4. Refuse a version mismatch between the engine package and this package.
16
+ // Silent lockstep drift produces ABI crashes that look like renderer bugs.
17
+ //
18
+ // ⛔ `__dirname` ALONE IS NOT ENOUGH UNDER link:/file: (P10, 2026-09-02)
19
+ //
20
+ // The original comment here said "never from CWD -- link, install and tarball
21
+ // must all work". The first two words were right and the last claim was not
22
+ // TESTED: no gate ever ran the resolver through a symlinked package, so the
23
+ // linked case was asserted in a comment and broken in fact.
24
+ //
25
+ // Under `file:`/`npm link`, node_modules/react-native-impeller-platform is a
26
+ // SYMLINK back to this source checkout. `__dirname` therefore lands in the
27
+ // checkout, whose node_modules does NOT contain the engine package -- the
28
+ // engine lives in the checkout's sibling `packages-engine/engine-<triple>/`,
29
+ // which is where tools/engine-build.sh writes it and which no node_modules
30
+ // walk from anywhere will ever reach. Reproduced: a linked consumer app with
31
+ // the engine package ALSO linked into its own node_modules still failed,
32
+ // because resolution started from the symlink TARGET, not the app.
33
+ //
34
+ // So the anchor list is ordered, not replaced:
35
+ // a. __dirname -- installed + tarball. Unchanged, first.
36
+ // b. INIT_CWD / process.cwd() -- the consumer's own tree, which is what
37
+ // __dirname stops pointing at under link:.
38
+ // c. <checkout>/packages-engine/ -- the linked source tree's own build
39
+ // output, read directly by path.
40
+ // (a) still wins in every shipped install, so this cannot change what a real
41
+ // consumer resolves. It only gives the linked case somewhere true to look.
42
+ //
43
+ // This is deliberately NOT "just set IMPELLER_ENGINE_BINARY in the app's
44
+ // script". Line 79 of this file already rules that out: a package that needs
45
+ // an env var to function has not shipped. That applies to our own dev loop
46
+ // too -- an env var in a downstream app's paint-proof script is a workaround
47
+ // that hides this defect instead of fixing it.
48
+
49
+ const fs = require('fs');
50
+ const path = require('path');
51
+
52
+ // ⛔ THIS SET IS A PROMISE, NOT A ROADMAP (P7, 2026-08-14)
53
+ //
54
+ // A triple listed here with no published engine package produces the WORST
55
+ // possible failure: npm silently skips a missing optionalDependency and exits
56
+ // 0, so the user gets a clean install with no engine, then hits the "Missing
57
+ // engine package" error below — which instructs them to install a package that
58
+ // does not exist. A good error message with an impossible instruction.
59
+ //
60
+ // So a triple joins this list when its engine package is BUILT, not when it is
61
+ // planned. `tools/ci-release-meta.sh` fails the build if the two ever disagree.
62
+ // darwin-arm64/darwin-x64/linux-arm64 belong here the moment P6 ships them.
63
+ const SUPPORTED = new Set([
64
+ 'linux-x64',
65
+ 'win32-x64',
66
+ 'darwin-arm64',
67
+ ]);
68
+
69
+ function hostTriple(platform = process.platform, arch = process.arch) {
70
+ return `${platform}-${arch}`;
71
+ }
72
+
73
+ function packageNameFor(triple) {
74
+ return `@blbaker/react-native-impeller-platform-engine-${triple}`;
75
+ }
76
+
77
+ function binaryFileName(platform = process.platform) {
78
+ return platform === 'win32' ? 'rn-impeller.exe' : 'rn-impeller';
79
+ }
80
+
81
+ function ownVersion() {
82
+ return require('../package.json').version;
83
+ }
84
+
85
+ /**
86
+ * Ordered list of places to look for the engine package's package.json.
87
+ *
88
+ * ⛔ ORDER IS THE WHOLE SAFETY ARGUMENT. __dirname is index 0, so for any
89
+ * normally-installed consumer this function returns on the first candidate and
90
+ * behaves exactly as it did before link: support existed. Everything after it
91
+ * is reached ONLY when the __dirname walk found nothing -- i.e. only in the
92
+ * case that used to throw.
93
+ */
94
+ function engineCandidates(triple, opts) {
95
+ const env = opts.env || process.env;
96
+ const cwd = opts.cwd || process.cwd();
97
+ const out = [];
98
+
99
+ // (a) Installed / tarball. Unchanged, and always first.
100
+ out.push({kind: 'resolve', from: __dirname, source: 'package'});
101
+
102
+ // (b) The consumer's own tree. Under link:/file: this is the only anchor that
103
+ // still points at the app, because __dirname followed the symlink out of
104
+ // it. npm sets INIT_CWD to the directory the command was run from, which
105
+ // survives npm running scripts from elsewhere; process.cwd() covers a
106
+ // plain `node ...` invocation.
107
+ const consumerRoots = [env.INIT_CWD, cwd].filter(Boolean);
108
+ for (const root of consumerRoots) {
109
+ out.push({kind: 'resolve', from: path.join(root, 'node_modules'), source: 'linked-consumer'});
110
+ }
111
+
112
+ // (c) The linked SOURCE CHECKOUT's own build output. package/src/resolve.js
113
+ // -> ../../packages-engine/engine-<triple>. This is a direct path probe,
114
+ // not a node_modules walk, because engine-build.sh writes the binary
115
+ // there and nothing ever installs it into a node_modules anywhere.
116
+ out.push({
117
+ kind: 'path',
118
+ // The directory is named by the triple, not the package: the npm name is
119
+ // scoped under a different prefix and does not match the folder.
120
+ dir: path.join(__dirname, '..', '..', 'packages-engine', `engine-${triple}`),
121
+ source: 'linked-checkout',
122
+ });
123
+
124
+ return out;
125
+ }
126
+
127
+ /**
128
+ * @returns {{binary: string, source: string, packageName: string|null, version: string|null}}
129
+ */
130
+ function resolveEngine(opts = {}) {
131
+ const platform = opts.platform || process.platform;
132
+ const arch = opts.arch || process.arch;
133
+ const env = opts.env || process.env;
134
+ const triple = hostTriple(platform, arch);
135
+
136
+ const override = env.IMPELLER_ENGINE_BINARY;
137
+ if (override) {
138
+ if (!fs.existsSync(override)) {
139
+ throw new Error(
140
+ `IMPELLER_ENGINE_BINARY is set to "${override}" but no file exists there.\n` +
141
+ 'Unset it to fall back to the installed engine package.'
142
+ );
143
+ }
144
+ return {binary: path.resolve(override), source: 'env', packageName: null, version: null};
145
+ }
146
+
147
+ if (!SUPPORTED.has(triple)) {
148
+ // ⛔ DO NOT TELL A CONSUMER TO BUILD THE ENGINE.
149
+ // This message used to lead with `export IMPELLER_ENGINE_BINARY=...`, which
150
+ // reads as "the supported fix is to compile a C++ engine yourself" — a
151
+ // 40-minute Hermes build, a native toolchain, and an env var the app now
152
+ // depends on forever. That is contributor work leaking into a consumer
153
+ // error path. A package that needs an env var to function has not shipped.
154
+ //
155
+ // Say what is true (this platform is not supported yet) and point at the
156
+ // one place that will change when it is. The env var is still honoured and
157
+ // still documented under contributing — it is simply not the answer we hand
158
+ // to someone whose install just failed.
159
+ throw new Error(
160
+ `react-native-impeller-platform does not support ${triple} yet.\n` +
161
+ `Supported: ${[...SUPPORTED].join(', ')}\n\n` +
162
+ 'Track or request platform support:\n' +
163
+ ' https://github.com/blbaker/react-native-impeller-platform/issues\n'
164
+ );
165
+ }
166
+
167
+ const pkgName = packageNameFor(triple);
168
+ let pkgJsonPath = null;
169
+ let source = null;
170
+ const tried = [];
171
+ for (const cand of engineCandidates(triple, {env, cwd: opts.cwd})) {
172
+ if (cand.kind === 'resolve') {
173
+ tried.push(cand.from);
174
+ try {
175
+ pkgJsonPath = require.resolve(`${pkgName}/package.json`, {paths: [cand.from]});
176
+ } catch (e) {
177
+ continue;
178
+ }
179
+ } else {
180
+ const p = path.join(cand.dir, 'package.json');
181
+ tried.push(cand.dir);
182
+ if (!fs.existsSync(p)) continue;
183
+ pkgJsonPath = p;
184
+ }
185
+ source = cand.source;
186
+ break;
187
+ }
188
+
189
+ if (!pkgJsonPath) {
190
+ throw new Error(
191
+ `Missing engine package "${pkgName}".\n` +
192
+ 'It ships as an optional dependency of react-native-impeller-platform and should\n' +
193
+ 'been installed automatically. Install it explicitly:\n\n' +
194
+ ` npm install ${pkgName}@${ownVersion()}\n\n` +
195
+ // ⛔ SAY WHERE WE LOOKED. The old message named the package but not the
196
+ // search path, so a linked checkout whose engine was simply never built
197
+ // produced the same text as a genuinely broken install.
198
+ 'Looked in:\n' +
199
+ tried.map((t) => ` ${t}`).join('\n') +
200
+ '\n\nOr point IMPELLER_ENGINE_BINARY at a locally built engine.'
201
+ );
202
+ }
203
+
204
+ const enginePkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
205
+ const expected = ownVersion();
206
+ if (enginePkg.version !== expected) {
207
+ throw new Error(
208
+ `Engine version mismatch: ${pkgName}@${enginePkg.version} is installed but\n` +
209
+ `react-native-impeller-platform@${expected} requires exactly ${expected}.\n` +
210
+ 'The engine C ABI is versioned in lockstep; mixed versions crash at runtime.\n\n' +
211
+ ` npm install ${pkgName}@${expected}`
212
+ );
213
+ }
214
+
215
+ const binary = path.join(path.dirname(pkgJsonPath), 'bin', binaryFileName(platform));
216
+ if (!fs.existsSync(binary)) {
217
+ throw new Error(
218
+ `Engine package "${pkgName}" is installed but its binary is missing:\n ${binary}\n` +
219
+ // A linked checkout reaches this when the package.json is committed but
220
+ // packages-engine/*/bin is gitignored and was never built. "Corrupt --
221
+ // reinstall it" is the wrong instruction there, so name the real fix.
222
+ (source === 'linked-checkout'
223
+ ? `This is a LINKED SOURCE CHECKOUT, not an installed package. Build the engine:\n` +
224
+ ` bash tools/engine-build.sh ${platform} ${arch}`
225
+ : 'The package is corrupt -- reinstall it.')
226
+ );
227
+ }
228
+ return {binary, source, packageName: pkgName, version: enginePkg.version};
229
+ }
230
+
231
+ module.exports = {
232
+ resolveEngine,
233
+ hostTriple,
234
+ packageNameFor,
235
+ binaryFileName,
236
+ engineCandidates,
237
+ SUPPORTED,
238
+ };