react-native-impeller-platform 0.0.0-stage → 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 +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/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,123 @@
1
+ /**
2
+ * Self-test for the engine resolver.
3
+ *
4
+ * Runs as `npm test` INSIDE THE PUBLISHED PACKAGE, which is the point: it is
5
+ * the one check a user can run on the artifact they actually installed, on a
6
+ * machine we have never seen. So it must depend on nothing but this package —
7
+ * no dev dependencies, no test runner, no network, no engine binary.
8
+ *
9
+ * package.json declared this script for the whole of P1-P6 while the file did
10
+ * not exist; `npm test` died with MODULE_NOT_FOUND. tools/ci-release-meta.sh
11
+ * now fails the build when a declared entry point is missing.
12
+ */
13
+ 'use strict';
14
+
15
+ const assert = require('assert');
16
+ const fs = require('fs');
17
+ const os = require('os');
18
+ const path = require('path');
19
+
20
+ const {resolveEngine, hostTriple, packageNameFor, binaryFileName, SUPPORTED} =
21
+ require('./resolve');
22
+
23
+ let passed = 0;
24
+ function check(name, fn) {
25
+ try {
26
+ fn();
27
+ console.log(` ok ${name}`);
28
+ passed++;
29
+ } catch (e) {
30
+ console.error(` FAIL ${name}\n ${e.message}`);
31
+ process.exitCode = 1;
32
+ }
33
+ }
34
+
35
+ console.log('react-native-impeller-platform selftest');
36
+
37
+ check('host triple is platform-arch', () => {
38
+ assert.strictEqual(hostTriple('linux', 'x64'), 'linux-x64');
39
+ assert.strictEqual(hostTriple('win32', 'x64'), 'win32-x64');
40
+ });
41
+
42
+ check('engine package name is derived from the triple', () => {
43
+ assert.strictEqual(
44
+ packageNameFor('linux-x64'),
45
+ '@blbaker/react-native-impeller-platform-engine-linux-x64'
46
+ );
47
+ });
48
+
49
+ check('binary name is .exe only on win32', () => {
50
+ assert.strictEqual(binaryFileName('linux'), 'rn-impeller');
51
+ assert.strictEqual(binaryFileName('darwin'), 'rn-impeller');
52
+ assert.strictEqual(binaryFileName('win32'), 'rn-impeller.exe');
53
+ });
54
+
55
+ check('every SUPPORTED triple names a real optionalDependency', () => {
56
+ // The defect this guards: a triple advertised as supported with no package
57
+ // published. npm SKIPS a missing optional dependency and exits 0, so the
58
+ // user gets a silent no-engine install and then an error instructing them to
59
+ // install something that does not exist.
60
+ const pkg = require('../package.json');
61
+ const opt = Object.keys(pkg.optionalDependencies || {});
62
+ for (const triple of SUPPORTED) {
63
+ assert.ok(
64
+ opt.includes(packageNameFor(triple)),
65
+ `${triple} is SUPPORTED but ${packageNameFor(triple)} is not an optionalDependency`
66
+ );
67
+ }
68
+ });
69
+
70
+ check('every engine optionalDependency is pinned exactly, never a range', () => {
71
+ // The engine exposes an unstable C ABI. A ^ or ~ range lets npm resolve a
72
+ // newer engine against an older host, which crashes at runtime rather than
73
+ // failing to install.
74
+ const pkg = require('../package.json');
75
+ for (const [name, spec] of Object.entries(pkg.optionalDependencies || {})) {
76
+ assert.strictEqual(spec, pkg.version, `${name} is pinned '${spec}', expected '${pkg.version}'`);
77
+ }
78
+ });
79
+
80
+ check('an unsupported triple is refused, without demanding the user build it', () => {
81
+ // ⛔ THIS TEST USED TO REQUIRE THE OPPOSITE.
82
+ // It asserted the error names IMPELLER_ENGINE_BINARY — i.e. it ENFORCED
83
+ // telling a consumer whose install just failed to go compile a C++ engine.
84
+ // A test can pin the wrong behaviour just as firmly as the right one; this
85
+ // one kept contributor workflow in a consumer error path.
86
+ assert.throws(
87
+ () => resolveEngine({platform: 'sunos', arch: 'sparc', env: {}}),
88
+ (e) =>
89
+ /does not support sunos-sparc yet/.test(e.message) &&
90
+ /Supported: /.test(e.message) &&
91
+ // The message must NOT hand a consumer a build instruction.
92
+ !/IMPELLER_ENGINE_BINARY/.test(e.message)
93
+ );
94
+ });
95
+
96
+ check('IMPELLER_ENGINE_BINARY overrides the installed package', () => {
97
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'rni-selftest-'));
98
+ const fake = path.join(tmp, 'rn-impeller');
99
+ fs.writeFileSync(fake, '');
100
+ try {
101
+ const r = resolveEngine({env: {IMPELLER_ENGINE_BINARY: fake}});
102
+ assert.strictEqual(r.source, 'env');
103
+ assert.strictEqual(r.binary, fake);
104
+ } finally {
105
+ fs.rmSync(tmp, {recursive: true, force: true});
106
+ }
107
+ });
108
+
109
+ check('a missing IMPELLER_ENGINE_BINARY fails loudly, not silently', () => {
110
+ // "Cannot find it" and "using the default" must never share an outcome: an
111
+ // override that silently falls back would run a DIFFERENT engine than the
112
+ // developer asked for, which is the hardest kind of bug to see.
113
+ assert.throws(
114
+ () => resolveEngine({env: {IMPELLER_ENGINE_BINARY: '/nonexistent/rn-impeller'}}),
115
+ /no file exists there/
116
+ );
117
+ });
118
+
119
+ console.log(
120
+ process.exitCode
121
+ ? 'SELFTEST_FAIL'
122
+ : `SELFTEST_PASS (${passed} checks)`
123
+ );
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+ // ENG-04 shape B, the JS half. An AsyncStorage-shaped API composed on top of
3
+ // the engine's ONE file primitive.
4
+ //
5
+ // WHY THIS LIVES IN JS
6
+ // -------------------
7
+ // The engine vends AppStorage.readFile/writeFile/deleteFile/listFiles and
8
+ // nothing more. That is a deliberate boundary: persistence is not a renderer
9
+ // concern, and if the engine owned a key-value API instead, every later need
10
+ // (structured values, namespacing, encryption, migration) would be a new C++
11
+ // row against a surface frozen in a native release. Here it is a JS change.
12
+ //
13
+ // Same division as Alert.impeller.js: the engine owns the CAPABILITY, JS owns
14
+ // the SHAPE.
15
+ //
16
+ // ⛔ ASYNC ON PURPOSE, OVER A SYNCHRONOUS PRIMITIVE.
17
+ // The native calls are synchronous (small flat files, called from the JS
18
+ // thread). This wrapper is async anyway, because async is the shape every RN
19
+ // storage consumer expects and the one you cannot retrofit later without
20
+ // breaking every caller. If the native half ever moves to a thread pool, no
21
+ // consumer of this module changes.
22
+
23
+ const {TurboModuleRegistry} = require('react-native');
24
+
25
+ // ⚠️ get, NOT getEnforcing. An engine built before ENG-04 does not vend
26
+ // AppStorage, and a hard throw at import time would take the whole bundle down
27
+ // on a version mismatch rather than failing the one feature. The null check in
28
+ // mod() turns that into a rejected Promise from the call that needed it.
29
+ const Native = TurboModuleRegistry.get('AppStorage');
30
+
31
+ function mod() {
32
+ if (Native == null) {
33
+ throw new Error(
34
+ 'AppStorage native module is unavailable. The engine binary predates ' +
35
+ 'ENG-04 (run `rn-impeller --list-modules` to confirm) — storage calls ' +
36
+ 'cannot work until the engine is updated.',
37
+ );
38
+ }
39
+ return Native;
40
+ }
41
+
42
+ // ⛔ KEY ENCODING IS NOT OPTIONAL AND IT IS NOT COSMETIC.
43
+ //
44
+ // The native side enforces a conservative allowlist ([A-Za-z0-9._-], one flat
45
+ // file per key, no nesting) because that is what keeps the container listable
46
+ // and the containment check simple. Real storage keys are not like that:
47
+ // '@myapp/user:profile' is an ordinary AsyncStorage key and would be REJECTED
48
+ // by the primitive.
49
+ //
50
+ // So keys are hex-encoded here. That is reversible, which listKeys() depends
51
+ // on, and it means no key can ever collide with another or escape the
52
+ // container -- a sanitising scheme (replace bad chars with '_') would silently
53
+ // map 'a/b' and 'a_b' onto ONE file, and the caller would never be told.
54
+ function encodeKey(key) {
55
+ if (typeof key !== 'string') {
56
+ throw new TypeError('storage key must be a string, got ' + typeof key);
57
+ }
58
+ let out = '';
59
+ for (let i = 0; i < key.length; i++) {
60
+ const code = key.charCodeAt(i);
61
+ // Encode the whole BMP code unit, not a byte: charCodeAt can return > 255
62
+ // for non-ASCII keys, and a 2-hex-digit assumption would truncate them
63
+ // into collisions ('\u0141' and 'A' both -> '41').
64
+ out += code.toString(16).padStart(4, '0');
65
+ }
66
+ return out + '.v1';
67
+ }
68
+
69
+ function decodeKey(name) {
70
+ if (!name.endsWith('.v1')) return null;
71
+ const body = name.slice(0, -3);
72
+ if (body.length % 4 !== 0) return null;
73
+ let out = '';
74
+ for (let i = 0; i < body.length; i += 4) {
75
+ const code = parseInt(body.slice(i, i + 4), 16);
76
+ if (Number.isNaN(code)) return null;
77
+ out += String.fromCharCode(code);
78
+ }
79
+ return out;
80
+ }
81
+
82
+ // A missing key resolves to null, never to ''. Those are different facts --
83
+ // "nothing stored" vs "an empty string was stored" -- and the native primitive
84
+ // preserves the distinction specifically so this layer can too. Collapsing
85
+ // them would make removeItem() indistinguishable from setItem(k, '').
86
+ async function getItem(key) {
87
+ return mod().readFile(encodeKey(key));
88
+ }
89
+
90
+ async function setItem(key, value) {
91
+ if (typeof value !== 'string') {
92
+ throw new TypeError(
93
+ 'storage values must be strings (stringify first), got ' + typeof value,
94
+ );
95
+ }
96
+ const ok = mod().writeFile(encodeKey(key), value);
97
+ if (!ok) {
98
+ // The primitive returns false for a rejected key, a full disk, or a failed
99
+ // atomic rename. It does not distinguish them, and neither can this layer
100
+ // honestly, so the error says what is known rather than guessing.
101
+ throw new Error('storage write failed for key: ' + key);
102
+ }
103
+ }
104
+
105
+ // Idempotent, matching the native half: removing an absent key is success, not
106
+ // an error, because that is the state the caller asked for.
107
+ async function removeItem(key) {
108
+ const ok = mod().deleteFile(encodeKey(key));
109
+ if (!ok) throw new Error('storage delete failed for key: ' + key);
110
+ }
111
+
112
+ // Enumeration comes from the DIRECTORY, not from an index file this module
113
+ // maintains. An index is a second source of truth that drifts the moment a
114
+ // write succeeds and the index update does not.
115
+ async function getAllKeys() {
116
+ const names = mod().listFiles();
117
+ const keys = [];
118
+ for (const name of names) {
119
+ const decoded = decodeKey(name);
120
+ // Skip anything this module did not write. A foreign file in the container
121
+ // is not an error and must not break enumeration for everything else.
122
+ if (decoded !== null) keys.push(decoded);
123
+ }
124
+ return keys;
125
+ }
126
+
127
+ async function clear() {
128
+ const keys = await getAllKeys();
129
+ for (const key of keys) {
130
+ await removeItem(key);
131
+ }
132
+ }
133
+
134
+ async function multiGet(keys) {
135
+ const out = [];
136
+ for (const key of keys) {
137
+ out.push([key, await getItem(key)]);
138
+ }
139
+ return out;
140
+ }
141
+
142
+ async function multiSet(pairs) {
143
+ for (const [key, value] of pairs) {
144
+ await setItem(key, value);
145
+ }
146
+ }
147
+
148
+ module.exports = {
149
+ getItem,
150
+ setItem,
151
+ removeItem,
152
+ getAllKeys,
153
+ clear,
154
+ multiGet,
155
+ multiSet,
156
+ // Exported for the gate: it asserts on the encoding round trip directly
157
+ // rather than inferring it from a get/set pair, so an encoding bug and a
158
+ // storage bug cannot be confused for each other.
159
+ encodeKey,
160
+ decodeKey,
161
+ };
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+ // FP-43 — react-native-view-shot's JS API on the engine's RNViewShot module.
3
+ //
4
+ // The engine implements view-shot's native module (captureRef, captureScreen,
5
+ // releaseCapture), so an app that depends on react-native-view-shot works as
6
+ // is; this file is the same surface for apps that do not. The snapshot renders
7
+ // the view's subtree into an offscreen on the raster thread, the png/jpg is
8
+ // encoded off the JS thread, and the Promise resolves on the JS thread.
9
+ //
10
+ // options: {format: 'png' | 'jpg', quality: 0..1, result: 'tmpfile' | 'base64'
11
+ // | 'data-uri', width, height (output pixels), fileName}.
12
+
13
+ const {TurboModuleRegistry, findNodeHandle} = require('react-native');
14
+
15
+ const Native = TurboModuleRegistry.get('RNViewShot');
16
+
17
+ function mod() {
18
+ if (Native == null) {
19
+ throw new Error(
20
+ 'RNViewShot native module is unavailable. The engine binary predates FP-43: ' +
21
+ 'view snapshots cannot work until the engine is updated.',
22
+ );
23
+ }
24
+ return Native;
25
+ }
26
+
27
+ const DEFAULTS = {format: 'png', quality: 1, result: 'tmpfile'};
28
+
29
+ // view-shot's validateOptions: defaults filled in, bad values reported.
30
+ function validateOptions(input) {
31
+ const options = {...DEFAULTS, ...(input || {})};
32
+ const errors = [];
33
+ if ('width' in options && (typeof options.width !== 'number' || options.width <= 0)) {
34
+ errors.push('option width should be a positive number');
35
+ delete options.width;
36
+ }
37
+ if ('height' in options && (typeof options.height !== 'number' || options.height <= 0)) {
38
+ errors.push('option height should be a positive number');
39
+ delete options.height;
40
+ }
41
+ if (typeof options.quality !== 'number' || options.quality < 0 || options.quality > 1) {
42
+ errors.push('option quality should be a number between 0.0 and 1.0');
43
+ options.quality = DEFAULTS.quality;
44
+ }
45
+ if (options.format === 'jpeg') options.format = 'jpg';
46
+ if (options.format !== 'png' && options.format !== 'jpg') {
47
+ errors.push(`option format '${options.format}' is not in valid formats: png | jpg`);
48
+ options.format = DEFAULTS.format;
49
+ }
50
+ if (!['tmpfile', 'base64', 'data-uri'].includes(options.result)) {
51
+ errors.push(`option result '${options.result}' is not in valid results: tmpfile | base64 | data-uri`);
52
+ options.result = DEFAULTS.result;
53
+ }
54
+ return {options, errors};
55
+ }
56
+
57
+ function toTag(view) {
58
+ if (view && typeof view === 'object' && 'current' in view) view = view.current;
59
+ if (typeof view === 'number') return view;
60
+ const tag = view == null ? null : findNodeHandle(view);
61
+ if (tag == null) throw new Error('findNodeHandle failed to resolve view=' + String(view));
62
+ return tag;
63
+ }
64
+
65
+ // Resolves with a file:// uri, base64 or a data uri (options.result).
66
+ function captureRef(view, optionsObject) {
67
+ let tag;
68
+ try {
69
+ tag = toTag(view);
70
+ } catch (e) {
71
+ return Promise.reject(e);
72
+ }
73
+ const {options, errors} = validateOptions(optionsObject);
74
+ if (__DEV__ && errors.length > 0) {
75
+ console.warn('react-native-view-shot captureRef options: ' + errors.join(', '));
76
+ }
77
+ return mod().captureRef(tag, options);
78
+ }
79
+
80
+ function captureScreen(optionsObject) {
81
+ const {options, errors} = validateOptions(optionsObject);
82
+ if (__DEV__ && errors.length > 0) {
83
+ console.warn('react-native-view-shot captureScreen options: ' + errors.join(', '));
84
+ }
85
+ return mod().captureScreen(options);
86
+ }
87
+
88
+ // Deletes a tmpfile capture.
89
+ function releaseCapture(uri) {
90
+ if (typeof uri !== 'string') return;
91
+ mod().releaseCapture(uri);
92
+ }
93
+
94
+ function isSupported() {
95
+ return Native != null;
96
+ }
97
+
98
+ module.exports = {captureRef, captureScreen, releaseCapture, isSupported};
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+ // ENG-09 — WorldRaster JS shim.
3
+ //
4
+ // The engine vends ONE primitive: WorldRaster.setPixels(id, rgba, w, h),
5
+ // synchronous, throwing on a mismatched buffer. This file is a thin,
6
+ // defensive wrapper -- same `get` (not `getEnforcing`) pattern as
7
+ // package/src/storage/index.js, so an engine built before ENG-09 fails the
8
+ // one call site that needs WorldRaster rather than taking the whole bundle
9
+ // down at import.
10
+
11
+ const {TurboModuleRegistry} = require('react-native');
12
+
13
+ const Native = TurboModuleRegistry.get('WorldRaster');
14
+
15
+ function mod() {
16
+ if (Native == null) {
17
+ throw new Error(
18
+ 'WorldRaster native module is unavailable. The engine binary predates ' +
19
+ 'ENG-09 — world raster calls cannot work until the engine is updated.',
20
+ );
21
+ }
22
+ return Native;
23
+ }
24
+
25
+ function isSupported() {
26
+ return Native != null && mod().isSupported();
27
+ }
28
+
29
+ // id: string — must match the `name` prop set on the <world> host component
30
+ // this buffer is meant for. rgba: ArrayBuffer of exactly w*h*4 bytes,
31
+ // tightly packed RGBA8, row-major, top-to-bottom (the engine performs the
32
+ // Y-flip at paint time; callers never flip).
33
+ function setPixels(id, rgba, w, h) {
34
+ mod().setPixels(id, rgba, w, h);
35
+ }
36
+
37
+ // ENG-10 — GPU sprite compositing.
38
+ //
39
+ // registerSprite(textureId, rgba, w, h): upload ONE atlas entry, once.
40
+ // rgba: ArrayBuffer of exactly w*h*4 bytes, STRAIGHT (non-premultiplied)
41
+ // RGBA8, top-to-bottom — canvas ImageData layout. Re-registering the same
42
+ // textureId replaces it.
43
+ function registerSprite(textureId, rgba, w, h) {
44
+ mod().registerSprite(textureId, rgba, w, h);
45
+ }
46
+
47
+ // drawSprites(id, sprites): per frame. `id` is the <world name> to composite
48
+ // over; sprites is an Array<{textureId, x, y, w, h, alpha?}> in layout points,
49
+ // BACK-TO-FRONT (list order is paint order). Throws on an unregistered
50
+ // textureId. Pass [] to clear.
51
+ function drawSprites(id, sprites) {
52
+ mod().drawSprites(id, sprites);
53
+ }
54
+
55
+ // ENG-21 — setSampling(id, mode): how <world name=id> samples its setPixels
56
+ // terrain and every sprite drawn on it. mode 'nearest' keeps pixel-art edges
57
+ // hard at an integer scale; 'linear' (the default) blends neighbouring texels.
58
+ // Throws on any other mode. Engines built before ENG-21 lack the method; call
59
+ // isSamplingSupported() first if the app must run on them.
60
+ function setSampling(id, mode) {
61
+ mod().setSampling(id, mode);
62
+ }
63
+
64
+ function isSamplingSupported() {
65
+ return Native != null && typeof Native.setSampling === 'function';
66
+ }
67
+
68
+ module.exports = {
69
+ isSupported,
70
+ isSamplingSupported,
71
+ setPixels,
72
+ registerSprite,
73
+ drawSprites,
74
+ setSampling,
75
+ };
@@ -0,0 +1,18 @@
1
+ # impeller/
2
+
3
+ Scaffolded by `npx react-native init-impeller` for **{{displayName}}**.
4
+
5
+ - `impeller.config.json` — window + app identity. The app name registered by the
6
+ engine is `{{appName}}`, taken from your `app.json`; the engine runs the same
7
+ name it registers.
8
+ - `build/` — Metro output for `--platform impeller`. Safe to delete; regenerated
9
+ by `run-impeller`. Add it to `.gitignore`.
10
+
11
+ The engine binary is **not** here. It is installed from npm as
12
+ `@blbaker/react-native-impeller-platform-engine-<platform>-<arch>` (an optional dependency of
13
+ `react-native-impeller-platform`) and located at runtime by the resolver. Set
14
+ `IMPELLER_ENGINE_BINARY` to point at a locally built engine instead.
15
+
16
+ Run it:
17
+
18
+ npx react-native run-impeller
@@ -0,0 +1 @@
1
+ build/
@@ -0,0 +1,11 @@
1
+ {
2
+ "//": "Scaffolded by init-impeller. App identity is derived from your app.json --",
3
+ "//2": "nothing here is hardcoded to a sample app, and no path is CWD-relative.",
4
+ "appName": "{{appName}}",
5
+ "displayName": "{{displayName}}",
6
+ "window": {
7
+ "width": 1024,
8
+ "height": 768,
9
+ "title": "{{displayName}}"
10
+ }
11
+ }