react-native-impeller-platform 0.0.0-stage → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -2
- package/package.json +52 -4
- package/react-native.config.js +24 -0
- package/src/bin/engine-path.js +12 -0
- package/src/cli/index.js +20 -0
- package/src/cli/init.js +112 -0
- package/src/cli/run.js +73 -0
- package/src/externaltexture/index.js +58 -0
- package/src/index.js +12 -0
- package/src/metro-shims/index.js +382 -0
- package/src/metro-shims/shims/Alert.impeller.js +89 -0
- package/src/metro-shims/shims/BaseViewConfig.impeller.js +81 -0
- package/src/metro-shims/shims/Button.impeller.js +100 -0
- package/src/metro-shims/shims/Image.impeller.js +97 -0
- package/src/metro-shims/shims/Platform.impeller-as-ios.js +44 -0
- package/src/metro-shims/shims/Platform.impeller.js +53 -0
- package/src/metro-shims/shims/ReactNativeFeatureFlags.impeller.js +40 -0
- package/src/metro-shims/shims/ReactNativeStyleAttributes.impeller.js +20 -0
- package/src/metro-shims/shims/TextInputState.impeller.js +100 -0
- package/src/metro-shims/shims/ViewConfigIgnore.impeller.js +55 -0
- package/src/metro-shims/shims/reanimated/clock.js +41 -0
- package/src/metro-shims/shims/reanimated/component.js +83 -0
- package/src/metro-shims/shims/reanimated/css.js +193 -0
- package/src/metro-shims/shims/reanimated/easing.js +47 -0
- package/src/metro-shims/shims/reanimated/hooks.js +191 -0
- package/src/metro-shims/shims/reanimated/interpolate.js +133 -0
- package/src/metro-shims/shims/reanimated/values.js +264 -0
- package/src/metro-shims/shims/reanimated.impeller.js +90 -0
- package/src/notifications/index.d.ts +44 -0
- package/src/notifications/index.js +95 -0
- package/src/platformview/index.js +23 -0
- package/src/resolve.js +238 -0
- package/src/selftest.js +123 -0
- package/src/storage/index.js +161 -0
- package/src/viewshot/index.js +98 -0
- package/src/worldraster/index.js +75 -0
- package/templates/app/README.md.tmpl +18 -0
- package/templates/app/_gitignore +1 -0
- package/templates/app/impeller.config.json.tmpl +11 -0
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
|
+
};
|
package/src/selftest.js
ADDED
|
@@ -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};
|