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
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
+ };
@@ -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};