@react-native-quickjs/quickjs 1.0.0-alpha.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 (84) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +31 -0
  4. package/README.md +217 -0
  5. package/ReactNativeQuickJS.podspec +197 -0
  6. package/android/CMakeLists.txt +134 -0
  7. package/android/build.gradle +214 -0
  8. package/android/quickjs.gradle +39 -0
  9. package/android/src/main/AndroidManifest.xml +2 -0
  10. package/android/src/main/java/com/reactnativequickjs/quickjs/QuickJSInstance.kt +39 -0
  11. package/android/src/main/java/com/reactnativequickjs/quickjs/QuickJSPackage.kt +32 -0
  12. package/android/src/main/jni/JJSRuntimeFactory.h +37 -0
  13. package/android/src/main/jni/JQuickJSInstance.cpp +27 -0
  14. package/android/src/main/jni/JQuickJSInstance.h +46 -0
  15. package/android/src/main/jni/OnLoad.cpp +15 -0
  16. package/app.plugin.js +11 -0
  17. package/apple/RCTQuickJSInstanceFactory.h +32 -0
  18. package/apple/RCTQuickJSInstanceFactory.mm +16 -0
  19. package/bin/qjsc/darwin-arm64/qjsc +0 -0
  20. package/bin/qjsc/darwin-x64/qjsc +0 -0
  21. package/bin/qjsc/linux-arm64/qjsc +0 -0
  22. package/bin/qjsc/linux-x64/qjsc +0 -0
  23. package/bin/qjsc/manifest.json +11 -0
  24. package/bin/qjsc/win32-x64/qjsc.exe +0 -0
  25. package/bin/react-native-quickjs.js +28 -0
  26. package/cmake/jsi.cmake +24 -0
  27. package/cmake/quickjs.cmake +125 -0
  28. package/engine/quickjs-rel/LICENSE +24 -0
  29. package/engine/quickjs-rel/MANIFEST.json +55 -0
  30. package/engine/quickjs-rel/builtin-array-fromasync.h +120 -0
  31. package/engine/quickjs-rel/builtin-iterator-zip-keyed.h +332 -0
  32. package/engine/quickjs-rel/builtin-iterator-zip.h +337 -0
  33. package/engine/quickjs-rel/cutils.h +1998 -0
  34. package/engine/quickjs-rel/dtoa.c +1619 -0
  35. package/engine/quickjs-rel/dtoa.h +87 -0
  36. package/engine/quickjs-rel/libregexp-opcode.h +73 -0
  37. package/engine/quickjs-rel/libregexp.c +3478 -0
  38. package/engine/quickjs-rel/libregexp.h +101 -0
  39. package/engine/quickjs-rel/libunicode-table.h +5173 -0
  40. package/engine/quickjs-rel/libunicode.c +2069 -0
  41. package/engine/quickjs-rel/libunicode.h +172 -0
  42. package/engine/quickjs-rel/list.h +107 -0
  43. package/engine/quickjs-rel/quickjs-atom.h +280 -0
  44. package/engine/quickjs-rel/quickjs-c-atomics.h +54 -0
  45. package/engine/quickjs-rel/quickjs-opcode.h +385 -0
  46. package/engine/quickjs-rel/quickjs.c +64854 -0
  47. package/engine/quickjs-rel/quickjs.h +1577 -0
  48. package/modules/cdp/quickjs-cdp.c +1386 -0
  49. package/modules/cdp/quickjs-cdp.h +122 -0
  50. package/modules/cdp/react/QuickJSInspector.cpp +322 -0
  51. package/modules/cdp/react/QuickJSInspector.h +114 -0
  52. package/modules/hermes-compat/include/hermes/DebuggerAPI.h +20 -0
  53. package/modules/hermes-compat/include/hermes/Public/CrashManager.h +47 -0
  54. package/modules/hermes-compat/include/hermes/Public/CtorConfig.h +88 -0
  55. package/modules/hermes-compat/include/hermes/Public/GCConfig.h +83 -0
  56. package/modules/hermes-compat/include/hermes/Public/HermesExport.h +17 -0
  57. package/modules/hermes-compat/include/hermes/Public/RuntimeConfig.h +66 -0
  58. package/modules/hermes-compat/include/hermes/Public/SamplingProfiler.h +22 -0
  59. package/modules/hermes-compat/include/hermes/hermes.h +195 -0
  60. package/modules/hermes-compat/include/hermes/inspector/RuntimeAdapter.h +43 -0
  61. package/modules/hermes-compat/include/hermes/inspector-modern/chrome/Registration.h +31 -0
  62. package/modules/hermes-compat/include/hermes-compat/Diagnostics.h +35 -0
  63. package/modules/hermes-compat/src/HermesCompat.cpp +557 -0
  64. package/package.json +104 -0
  65. package/scripts/bytecode/compile.js +99 -0
  66. package/scripts/expo/plugin.js +231 -0
  67. package/scripts/postinstall.js +74 -0
  68. package/scripts/react_native_quickjs_pods.rb +237 -0
  69. package/scripts/setup/edits.js +311 -0
  70. package/scripts/setup/run.js +121 -0
  71. package/src/bytecode/QuickJSBytecode.cpp +50 -0
  72. package/src/bytecode/QuickJSBytecode.h +50 -0
  73. package/src/module/QuickJSCompat.cpp +92 -0
  74. package/src/module/QuickJSCompat.h +19 -0
  75. package/src/module/QuickJSModule.cpp +70 -0
  76. package/src/module/QuickJSModule.h +108 -0
  77. package/src/module/QuickJSModuleNative.h +93 -0
  78. package/src/runtime/QuickJSInstance.cpp +37 -0
  79. package/src/runtime/QuickJSInstance.h +74 -0
  80. package/src/runtime/QuickJSRuntime.cpp +1864 -0
  81. package/src/runtime/QuickJSRuntime.h +529 -0
  82. package/src/runtime/QuickJSRuntimeConfig.h +103 -0
  83. package/src/runtime/QuickJSRuntimeFactory.cpp +34 -0
  84. package/src/runtime/QuickJSRuntimeFactory.h +24 -0
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright (c) Ammar Ahmed.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ *
8
+ * Compiles a Metro bundle to QuickJS bytecode, in place. Run from the Xcode
9
+ * bundle phase and the Gradle bundle task; see README.md.
10
+ *
11
+ * node scripts/bytecode/compile.js <bundle>
12
+ *
13
+ * Bytecode is only loadable by the engine build that produced it, so the
14
+ * compiler is pinned to the engine by BC_VERSION. A mismatch stops the build:
15
+ * shipping a bundle this engine cannot read fails at launch, on a user's
16
+ * device, with nothing to point at.
17
+ *
18
+ * Missing compiler for the host is not an error. The bundle is left as
19
+ * JavaScript, which runs -- just without the startup that bytecode buys.
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const { execFileSync } = require('child_process');
27
+
28
+ const ROOT = path.join(__dirname, '..', '..');
29
+ const COMPILERS = path.join(ROOT, 'bin', 'qjsc');
30
+
31
+ /** The BC_VERSION the shipped engine sources define. */
32
+ function engineBytecodeVersion() {
33
+ const engine = fs.readFileSync(path.join(ROOT, 'engine', 'quickjs-rel', 'quickjs.c'), 'utf8');
34
+ const found = /^#define BC_VERSION (\d+)/m.exec(engine);
35
+ if (!found) throw new Error('no BC_VERSION in engine/quickjs-rel/quickjs.c');
36
+ return Number(found[1]);
37
+ }
38
+
39
+ /** e.g. darwin-arm64. Matches the directory names the CI workflow writes. */
40
+ function hostPlatform() {
41
+ return `${process.platform}-${process.arch}`;
42
+ }
43
+
44
+ function compilerPath() {
45
+ const name = process.platform === 'win32' ? 'qjsc.exe' : 'qjsc';
46
+ return path.join(COMPILERS, hostPlatform(), name);
47
+ }
48
+
49
+ function skip(reason) {
50
+ console.log(`[quickjs] ${reason}; leaving the bundle as JavaScript.`);
51
+ process.exit(0);
52
+ }
53
+
54
+ function main(bundle) {
55
+ if (!bundle) {
56
+ console.error('usage: compile.js <bundle>');
57
+ process.exit(2);
58
+ }
59
+ if (!fs.existsSync(bundle)) skip(`${bundle} does not exist`);
60
+
61
+ const compiler = compilerPath();
62
+ if (!fs.existsSync(compiler)) skip(`no bytecode compiler for ${hostPlatform()}`);
63
+
64
+ // Zipped artifacts lose the executable bit, so a compiler can arrive in the
65
+ // repository unrunnable. Restoring it here is cheaper than the EACCES this
66
+ // would otherwise become.
67
+ try {
68
+ fs.accessSync(compiler, fs.constants.X_OK);
69
+ } catch {
70
+ fs.chmodSync(compiler, 0o755);
71
+ }
72
+
73
+ const manifestPath = path.join(COMPILERS, 'manifest.json');
74
+ const manifest = fs.existsSync(manifestPath)
75
+ ? JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
76
+ : {};
77
+ const engineVersion = engineBytecodeVersion();
78
+
79
+ if (manifest.bcVersion !== engineVersion) {
80
+ console.error(
81
+ `\n[quickjs] the bytecode compiler is stale.\n` +
82
+ ` compiler built for BC_VERSION ${manifest.bcVersion}\n` +
83
+ ` engine expects BC_VERSION ${engineVersion}\n\n` +
84
+ `Bytecode from this compiler would not load. Rebuild the compilers\n` +
85
+ `(the "bytecode compilers" workflow) or unset RNQJS_BYTECODE.\n`
86
+ );
87
+ process.exit(1);
88
+ }
89
+
90
+ const compiled = `${bundle}.qbc`;
91
+ const args = process.env.RNQJS_BYTECODE_KEEP_SOURCE ? [] : ['--strip-source'];
92
+ execFileSync(compiler, [...args, bundle, compiled], { stdio: 'inherit' });
93
+ fs.renameSync(compiled, bundle);
94
+
95
+ const size = fs.statSync(bundle).size;
96
+ console.log(`[quickjs] compiled ${path.basename(bundle)} to bytecode (${size} bytes)`);
97
+ }
98
+
99
+ main(process.argv[2]);
@@ -0,0 +1,231 @@
1
+ /*
2
+ * Copyright (c) Ammar Ahmed.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * The Expo config plugin. It runs the same edits as the CLI, but through
8
+ * Expo's mods, so a prebuild regenerates them rather than losing them.
9
+ *
10
+ * { "expo": { "plugins": ["@react-native-quickjs/quickjs"] } }
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const {
18
+ withAppDelegate,
19
+ withDangerousMod,
20
+ withGradleProperties,
21
+ withMainApplication,
22
+ withAppBuildGradle,
23
+ } = require('@expo/config-plugins');
24
+
25
+ const { edits } = require('../setup/edits');
26
+
27
+ const editFor = (label) => edits.find((edit) => edit.label === label);
28
+
29
+ function warnCouldNotEdit(label) {
30
+ console.warn(
31
+ `[@react-native-quickjs/quickjs] could not edit ${label}. ` +
32
+ 'Run `npx react-native-quickjs doctor` after prebuild.'
33
+ );
34
+ }
35
+
36
+ /** Run one edit from edits.js over the contents of a string mod. */
37
+ function addQuickJS(label, source) {
38
+ const edit = editFor(label);
39
+ if (edit.isApplied(source)) return source;
40
+
41
+ const edited = edit.addQuickJS(source);
42
+ if (edited == null) {
43
+ warnCouldNotEdit(label);
44
+ return source;
45
+ }
46
+ return edited;
47
+ }
48
+
49
+ // withGradleProperties hands over parsed items rather than text, so this one
50
+ // does not go through edits.js.
51
+ function withHermesOff(config) {
52
+ return withGradleProperties(config, (cfg) => {
53
+ const items = cfg.modResults.filter(
54
+ (i) => !(i.type === 'property' && i.key === 'hermesEnabled')
55
+ );
56
+ items.push({ type: 'property', key: 'hermesEnabled', value: 'false' });
57
+ cfg.modResults = items;
58
+ return cfg;
59
+ });
60
+ }
61
+
62
+ const withNoEngineDependency = (config) =>
63
+ withAppBuildGradle(config, (cfg) => {
64
+ cfg.modResults.contents = addQuickJS('android/app/build.gradle', cfg.modResults.contents);
65
+ return cfg;
66
+ });
67
+
68
+ const withAndroidFactory = (config) =>
69
+ withMainApplication(config, (cfg) => {
70
+ cfg.modResults.contents = addQuickJS('MainApplication.kt', cfg.modResults.contents);
71
+ return cfg;
72
+ });
73
+
74
+ const withIosFactory = (config) =>
75
+ withAppDelegate(config, (cfg) => {
76
+ cfg.modResults.contents = addQuickJS('AppDelegate.swift', cfg.modResults.contents);
77
+ return cfg;
78
+ });
79
+
80
+ // There is no withPodfile, so the Podfile is edited on disk after prebuild has
81
+ // written it.
82
+ const withPodfile = (config) =>
83
+ withDangerousMod(config, [
84
+ 'ios',
85
+ (cfg) => {
86
+ const file = path.join(cfg.modRequest.platformProjectRoot, 'Podfile');
87
+ if (!fs.existsSync(file)) {
88
+ warnCouldNotEdit('ios/Podfile');
89
+ return cfg;
90
+ }
91
+ fs.writeFileSync(file, addQuickJS('ios/Podfile', fs.readFileSync(file, 'utf8')));
92
+ return cfg;
93
+ },
94
+ ]);
95
+
96
+ // Expo's ReactHost delegate hardcodes the engine:
97
+ //
98
+ // override val jsRuntimeFactory: JSRuntimeFactory
99
+ // get() = HermesInstance()
100
+ //
101
+ // ExpoReactHostFactory.getDefaultReactHost does take a jsRuntimeFactory
102
+ // parameter, and MainApplication.kt passes ours to it, but the function never
103
+ // hands it to the delegate it builds. On Expo the argument is accepted and
104
+ // ignored, and the app dies at launch looking for libhermestooling.so.
105
+ //
106
+ // So the parameter is carried the rest of the way here, which is what Expo
107
+ // itself would do. Only React Native types appear in the patch: the `expo`
108
+ // Gradle module does not depend on ours, so naming QuickJSInstance in that file
109
+ // does not compile. The concrete factory is still built by MainApplication.kt,
110
+ // in the app module, which does depend on ours.
111
+ //
112
+ // This edits node_modules, so a reinstall undoes it. Prebuild again after one.
113
+ const EXPO_REACT_HOST_FACTORY =
114
+ 'expo/android/src/main/java/expo/modules/ExpoReactHostFactory.kt';
115
+
116
+ const EXPO_ENGINE_PATCH = [
117
+ [
118
+ ' private val hostHandlers: List<ReactNativeHostHandler>\n ) : ReactHostDelegate {',
119
+ ' private val hostHandlers: List<ReactNativeHostHandler>,\n' +
120
+ ' private val jsRuntimeFactoryOverride: JSRuntimeFactory? = null\n' +
121
+ ' ) : ReactHostDelegate {',
122
+ ],
123
+ [
124
+ ' hostHandlers = hostHandlers\n )',
125
+ ' hostHandlers = hostHandlers,\n' +
126
+ ' jsRuntimeFactoryOverride = jsRuntimeFactory\n )',
127
+ ],
128
+ [
129
+ ' override val jsRuntimeFactory: JSRuntimeFactory\n get() = HermesInstance()',
130
+ ' override val jsRuntimeFactory: JSRuntimeFactory\n' +
131
+ ' get() = jsRuntimeFactoryOverride ?: HermesInstance()',
132
+ ],
133
+ ];
134
+
135
+ const withExpoAndroidEngine = (config) =>
136
+ withDangerousMod(config, [
137
+ 'android',
138
+ (cfg) => {
139
+ const file = path.join(cfg.modRequest.projectRoot, 'node_modules', EXPO_REACT_HOST_FACTORY);
140
+ if (!fs.existsSync(file)) return cfg;
141
+
142
+ const source = fs.readFileSync(file, 'utf8');
143
+ if (source.includes('jsRuntimeFactoryOverride')) return cfg;
144
+
145
+ // All three or none: a half-applied patch does not compile.
146
+ if (!EXPO_ENGINE_PATCH.every(([find]) => source.includes(find))) {
147
+ console.warn(
148
+ '[@react-native-quickjs/quickjs] this version of Expo builds its ReactHost ' +
149
+ 'differently than the plugin expects, so the app would launch on Hermes. ' +
150
+ 'Please report this against @react-native-quickjs/quickjs.'
151
+ );
152
+ return cfg;
153
+ }
154
+
155
+ fs.writeFileSync(
156
+ file,
157
+ EXPO_ENGINE_PATCH.reduce((text, [find, replace]) => text.replace(find, replace), source)
158
+ );
159
+ return cfg;
160
+ },
161
+ ]);
162
+
163
+ // ExpoModulesCore.podspec picks its engine by reading ENV['USE_HERMES'] itself
164
+ // rather than calling React Native's use_hermes(), and it knows only two
165
+ // answers: Hermes, or React-jsc. use_quickjs! sets USE_HERMES=0 truthfully, so
166
+ // without this patch Expo resolves that to JavaScriptCore -- an engine the app
167
+ // never instantiates, whose JSI runtime it would still compile and link.
168
+ //
169
+ // Taught here as a third branch on USE_THIRD_PARTY_JSC, which is React Native's
170
+ // own name for "this app brought its own engine", so Expo depends on neither.
171
+ // jsinspector is still needed and comes from where the Hermes branch takes it.
172
+ //
173
+ // Safe because nothing in expo-modules-core's iOS sources mentions Hermes --
174
+ // the package's only makeHermesRuntime call is in its Android JNI -- so the
175
+ // -DUSE_HERMES that this branch also drops gates nothing in this pod.
176
+ //
177
+ // This is the change proposed upstream; when it lands, this mod goes away.
178
+ // It edits node_modules, so a reinstall undoes it. Prebuild again after one.
179
+ const EXPO_PODSPEC = 'expo-modules-core/ExpoModulesCore.podspec';
180
+
181
+ const EXPO_ENGINE_BRANCH = ` if use_hermes
182
+ s.dependency 'hermes-engine'
183
+ add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern')
184
+ else
185
+ s.dependency 'React-jsc'
186
+ end`;
187
+
188
+ const EXPO_ENGINE_BRANCH_PATCHED = ` if ENV['USE_THIRD_PARTY_JSC'] == '1'
189
+ add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern')
190
+ elsif use_hermes
191
+ s.dependency 'hermes-engine'
192
+ add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern')
193
+ else
194
+ s.dependency 'React-jsc'
195
+ end`;
196
+
197
+ const withExpoIosEngine = (config) =>
198
+ withDangerousMod(config, [
199
+ 'ios',
200
+ (cfg) => {
201
+ const file = path.join(cfg.modRequest.projectRoot, 'node_modules', EXPO_PODSPEC);
202
+ if (!fs.existsSync(file)) return cfg;
203
+
204
+ const source = fs.readFileSync(file, 'utf8');
205
+ if (source.includes(EXPO_ENGINE_BRANCH_PATCHED)) return cfg;
206
+
207
+ if (!source.includes(EXPO_ENGINE_BRANCH)) {
208
+ console.warn(
209
+ '[@react-native-quickjs/quickjs] this version of Expo picks its engine ' +
210
+ 'dependency differently than the plugin expects, so the app will also ' +
211
+ 'ship an engine it never runs. It will still run on QuickJS.'
212
+ );
213
+ return cfg;
214
+ }
215
+
216
+ fs.writeFileSync(file, source.replace(EXPO_ENGINE_BRANCH, EXPO_ENGINE_BRANCH_PATCHED));
217
+ return cfg;
218
+ },
219
+ ]);
220
+
221
+ module.exports = function withQuickJS(config) {
222
+ return [
223
+ withHermesOff,
224
+ withNoEngineDependency,
225
+ withAndroidFactory,
226
+ withIosFactory,
227
+ withPodfile,
228
+ withExpoAndroidEngine,
229
+ withExpoIosEngine,
230
+ ].reduce((c, mod) => mod(c), config);
231
+ };
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright (c) Ammar Ahmed.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ *
8
+ * postinstall, and why it is a script rather than a shell one-liner.
9
+ *
10
+ * This package is installed in two situations that need opposite behaviour:
11
+ *
12
+ * 1. THIS REPOSITORY, from git. engine/quickjs-ng is a submodule that has to
13
+ * be initialised and patched, or nothing builds.
14
+ *
15
+ * 2. A CONSUMER'S node_modules, from the npm tarball. That ships
16
+ * engine/quickjs-rel already patched, there is no submodule, and there is
17
+ * no git repository.
18
+ *
19
+ * Running the first in the second breaks three ways at once: `git submodule
20
+ * update` with no .git of its own walks UP out of node_modules and operates on
21
+ * the consuming app's repository; engine/patches is not published, so the patch
22
+ * script has nothing to apply; and shelling out to a package manager assumes
23
+ * one the consumer may not use.
24
+ *
25
+ * So detect the situation, and in (2) do nothing at all.
26
+ */
27
+
28
+ 'use strict';
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+ const { spawnSync } = require('child_process');
33
+
34
+ const root = path.join(__dirname, '..');
35
+
36
+ // engine/patches is absent from the tarball on purpose, and its absence is the
37
+ // signal. Checking for .git would be wrong: a consumer who vendors this package
38
+ // inside their own repository has a .git above them.
39
+ if (!fs.existsSync(path.join(root, 'engine', 'patches'))) {
40
+ process.exit(0);
41
+ }
42
+
43
+ function run(label, command, args) {
44
+ const result = spawnSync(command, args, {
45
+ cwd: root,
46
+ stdio: 'inherit',
47
+ encoding: 'utf8',
48
+ });
49
+
50
+ if (result.error || result.status !== 0) {
51
+ console.error(
52
+ `\n[react-native-quickjs] ${label} failed.\n` +
53
+ ` ${command} ${args.join(' ')}\n\n` +
54
+ 'The engine sources are not usable until this succeeds. Run it by hand\n' +
55
+ 'to see the full error.\n'
56
+ );
57
+ process.exit(1);
58
+ }
59
+ }
60
+
61
+ // --recursive matters: quickjs-ng has submodules of its own for its test
62
+ // suites, and a partial checkout fails later in cmake rather than here.
63
+ run('submodule checkout', 'git', [
64
+ 'submodule',
65
+ 'update',
66
+ '--init',
67
+ '--recursive',
68
+ ]);
69
+
70
+ // Invoked through node rather than a package manager, so this behaves the same
71
+ // under npm, yarn, pnpm and bun.
72
+ run('patch application', process.execPath, [
73
+ path.join(root, 'scripts', 'apply-patches.js'),
74
+ ]);
@@ -0,0 +1,237 @@
1
+ # Copyright (c) Ammar Ahmed.
2
+ #
3
+ # This source code is licensed under the MIT license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+ #
6
+ # Podfile helper for apps running on QuickJS.
7
+ #
8
+ # require_relative '../node_modules/@react-native-quickjs/quickjs/scripts/react_native_quickjs_pods.rb'
9
+ #
10
+ # target 'App' do
11
+ # use_quickjs! # before use_react_native!
12
+ # use_react_native!(:path => config[:reactNativePath])
13
+ #
14
+ # post_install do |installer|
15
+ # react_native_post_install(installer, config[:reactNativePath])
16
+ # react_native_quickjs_post_install(installer) # after
17
+ # end
18
+ # end
19
+
20
+ # react-native-worklets and react-native-reanimated declare
21
+ # `s.dependency 'React-hermes'` unconditionally, and use_quickjs! never declares
22
+ # that pod, so `pod install` cannot resolve it. Neither uses anything from it:
23
+ # their only Hermes include is <hermes/hermes.h>, which the compatibility shim
24
+ # provides.
25
+ #
26
+ # Dropped as each podspec is read, rather than by editing node_modules, so a
27
+ # reinstall does not undo it. React Native's own podspecs are not affected: they
28
+ # declare the same dependency behind `if use_hermes()`, which is already false.
29
+ #
30
+ # Only done when the shim is installed, so that without it a library asking for
31
+ # the real Hermes still fails loudly rather than silently losing it.
32
+ def react_native_quickjs_drop_react_hermes
33
+ return if ENV["RNQJS_HERMES_COMPAT"] == "0"
34
+
35
+ dependency = Pod::Specification.instance_method(:dependency)
36
+ Pod::Specification.send(:define_method, :dependency) do |*args, &block|
37
+ next if args.first.to_s == "React-hermes"
38
+
39
+ dependency.bind(self).call(*args, &block)
40
+ end
41
+ end
42
+
43
+ # Removes Hermes. Must run before use_react_native!, which reads all of this as
44
+ # the podspecs are evaluated.
45
+ def use_quickjs!
46
+ # Turns off every `if use_hermes()` dependency on hermes-engine at once.
47
+ # React Native's own use_hermes() is `!use_third_party_jsc()`, so this one
48
+ # flag answers for every React Native pod.
49
+ ENV['USE_THIRD_PARTY_JSC'] = '1'
50
+
51
+ # Set as well, because a podspec is free to read it directly rather than call
52
+ # use_hermes() -- Expo's does -- and to such a reader an unset USE_HERMES
53
+ # means "yes, Hermes". Leaving it unset would have this app claim an engine it
54
+ # does not have, and any library added later would be told the same.
55
+ #
56
+ # Safe alongside the flag above: error_if_try_to_use_jsc_from_core aborts on
57
+ # USE_HERMES=0 only while USE_THIRD_PARTY_JSC is unset or 0, which is the
58
+ # "asking for the JavaScriptCore that used to be in core" case, not this one.
59
+ #
60
+ # A reader that has only two engines in mind resolves this to JavaScriptCore
61
+ # rather than to no engine. That is what the Expo config plugin's podspec
62
+ # patch is for: it teaches USE_THIRD_PARTY_JSC as a third answer.
63
+ ENV['USE_HERMES'] = '0'
64
+
65
+ react_native_quickjs_drop_react_hermes
66
+
67
+ # On the prebuilt path hermesvm.framework carries the JSI implementation, and
68
+ # React-jsi.podspec drops its own jsi.cpp whenever Hermes is on. Removing
69
+ # Hermes there leaves every runtime, ours included, unable to link. Consumers
70
+ # pay for this in first-build and cold CI time.
71
+ ENV['RCT_USE_PREBUILT_RNCORE'] = '0'
72
+
73
+ # These declare Hermes pods in the Podfile directly, so they never consult
74
+ # use_hermes(). use_react_native! reaches them through `hermes_enabled`, which
75
+ # react_native_pods.rb:81 assigns true unconditionally -- replacing the
76
+ # functions is the only way to stop them.
77
+ Object.send(:define_method, :setup_hermes!) { |**_| }
78
+ Object.send(:define_method, :depend_on_js_engine) { |_spec| }
79
+
80
+ bridgeless = Object.instance_method(:setup_bridgeless!)
81
+ Object.send(:define_method, :setup_bridgeless!) do |**kwargs|
82
+ bridgeless.bind(self).call(**kwargs.merge(:use_hermes => false))
83
+ end
84
+
85
+ Pod::UI.puts(
86
+ "[ReactNativeQuickJS] Hermes removed — JavaScript runs on QuickJS.".green
87
+ )
88
+ end
89
+
90
+ # Compiles the release bundle to QuickJS bytecode, in a build phase placed
91
+ # right after React Native's own bundling phase. Debug builds load JavaScript
92
+ # from Metro and never produce a bundle, so the script exits early there.
93
+ BYTECODE_PHASE = '[ReactNativeQuickJS] Compile JavaScript to bytecode'
94
+
95
+ BYTECODE_SCRIPT = <<~SH
96
+ set -e
97
+ [ "$CONFIGURATION" = "Debug" ] && exit 0
98
+ [ "$RNQJS_BYTECODE" = "0" ] && exit 0
99
+
100
+ # The same files React Native's own bundle phase reads NODE_BINARY from.
101
+ [ -f "$SRCROOT/.xcode.env" ] && . "$SRCROOT/.xcode.env"
102
+ [ -f "$SRCROOT/.xcode.env.local" ] && . "$SRCROOT/.xcode.env.local"
103
+ : "${NODE_BINARY:=node}"
104
+
105
+ # Resolved through node, so a hoisted node_modules is found.
106
+ COMPILER=$("$NODE_BINARY" --print \
107
+ "require.resolve('@react-native-quickjs/quickjs/scripts/bytecode/compile.js', {paths: ['$SRCROOT']})")
108
+
109
+ "$NODE_BINARY" "$COMPILER" \
110
+ "$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/main.jsbundle"
111
+ SH
112
+
113
+ def react_native_quickjs_add_bytecode_phase(installer)
114
+ installer.aggregate_targets.map(&:user_project).uniq(&:path).compact.each do |project|
115
+ project.native_targets.each do |target|
116
+ next unless target.product_type == 'com.apple.product-type.application'
117
+ next if target.build_phases.any? { |phase| phase.display_name == BYTECODE_PHASE }
118
+
119
+ phase = target.new_shell_script_build_phase(BYTECODE_PHASE)
120
+ phase.shell_script = BYTECODE_SCRIPT
121
+
122
+ # Ordering matters: the bundle has to exist before it can be compiled.
123
+ after = target.build_phases.index do |other|
124
+ other.display_name.to_s.include?('Bundle React Native code and images')
125
+ end
126
+ next if after.nil?
127
+
128
+ target.build_phases.delete(phase)
129
+ target.build_phases.insert(after + 1, phase)
130
+ end
131
+ project.save
132
+ end
133
+ end
134
+
135
+ def react_native_quickjs_post_install(installer)
136
+ react_native_quickjs_add_bytecode_phase(installer)
137
+
138
+ # Debug only, matching the gate React Native puts on its own inspector.
139
+ # QuickJSInstance::debuggerEnabledByDefault() already refuses to attach in a
140
+ # release build, so this keeps the compiled surface in agreement.
141
+ react_native_quickjs_append(
142
+ installer, "ReactNativeQuickJS", "GCC_PREPROCESSOR_DEFINITIONS",
143
+ "RNQJS_ENABLE_CDP=1", :debug_only => true
144
+ )
145
+
146
+ # RCTCxxBridge.mm imports Hermes under `#if !defined(USE_HERMES)`, and
147
+ # js_engine_flags() never defines it in the non-Hermes case.
148
+ react_native_quickjs_append(
149
+ installer, "React-Core", "GCC_PREPROCESSOR_DEFINITIONS", "USE_HERMES=0"
150
+ )
151
+
152
+ # RCTAppSetupUtils.h imports Hermes under `#if USE_THIRD_PARTY_JSC != 1`, and
153
+ # React-RCTAppDelegate.podspec:21 loses the flag to a missing space -- clang
154
+ # receives the two joined, as -DRCT_NEW_ARCH_ENABLED=1-DUSE_THIRD_PARTY_JSC=1.
155
+ #
156
+ # Every pod target, not just React-RCTAppDelegate: any pod including that
157
+ # header resolves the same #if, and Expo's does, through RCTAppDelegateUmbrella
158
+ # -- so a targeted define builds a plain app and fails an Expo one.
159
+ react_native_quickjs_append_all(
160
+ installer, "GCC_PREPROCESSOR_DEFINITIONS", "USE_THIRD_PARTY_JSC=1"
161
+ )
162
+
163
+ # With the Hermes compatibility shim installed, every pod must be able to
164
+ # resolve <hermes/hermes.h> -- react-native-worklets decides which engine it
165
+ # is built for with __has_include on exactly that path. The headers cannot be
166
+ # published as public headers of this pod; see the HermesCompat subspec.
167
+ if ENV["RNQJS_HERMES_COMPAT"] != "0"
168
+ shim = File.expand_path("../modules/hermes-compat/include", __dir__)
169
+ react_native_quickjs_append_all(installer, "HEADER_SEARCH_PATHS", "\"#{shim}\"")
170
+ react_native_quickjs_append_all(
171
+ installer, "GCC_PREPROCESSOR_DEFINITIONS", "HERMES_ENABLE_DEBUGGER=1"
172
+ )
173
+ end
174
+
175
+ # createJSRuntimeFactory has an empty body under USE_THIRD_PARTY_JSC=1, which
176
+ # -Werror,-Wreturn-type rejects. Reachable only in an app that does not
177
+ # override it, and overriding it is how an app selects QuickJS at all.
178
+ react_native_quickjs_append(
179
+ installer, "React-RCTAppDelegate", "OTHER_CFLAGS", "-Wno-error=return-type"
180
+ )
181
+
182
+ # react-native-xcode.sh compares against the literal text `false`, so `0`,
183
+ # `NO` and `FALSE` all read as "use Hermes" and a release build ships a
184
+ # bytecode bundle QuickJS cannot execute.
185
+ installer.aggregate_targets
186
+ .map { |t| t.user_project }
187
+ .uniq { |p| p.path }
188
+ .push(installer.pods_project)
189
+ .compact
190
+ .each do |project|
191
+ project.build_configurations.each do |config|
192
+ config.build_settings["USE_HERMES"] = "false"
193
+ end
194
+ project.save()
195
+ end
196
+
197
+ Pod::UI.puts(
198
+ "[ReactNativeQuickJS] USE_HERMES=false — release bundles are compiled to " \
199
+ "QuickJS bytecode, not Hermes bytecode.".green
200
+ )
201
+ end
202
+
203
+ # Raises when the pod is absent. Every caller repairs something that otherwise
204
+ # breaks the build, and they match React Native's pods by name -- exactly what a
205
+ # version bump renames -- so a silent no-op would resurface much later as the
206
+ # error it was meant to prevent.
207
+ # Applies a setting to every pod target. Used for a flag that selects which
208
+ # JavaScript engine header a React Native header imports: any pod that includes
209
+ # it needs the same answer, and which pods those are is not knowable here.
210
+ def react_native_quickjs_append_all(installer, setting, value)
211
+ installer.target_installation_results.pod_target_installation_results.each_value do |result|
212
+ result.native_target.build_configurations.each do |config|
213
+ current = config.build_settings[setting] || "$(inherited)"
214
+ current = current.join(" ") if current.is_a?(Array)
215
+ config.build_settings[setting] = "#{current} #{value}"
216
+ end
217
+ end
218
+ end
219
+
220
+ def react_native_quickjs_append(installer, pod_name, setting, value, debug_only: false)
221
+ result = installer.target_installation_results
222
+ .pod_target_installation_results[pod_name]
223
+
224
+ if result.nil?
225
+ raise "[ReactNativeQuickJS] no pod named #{pod_name}, so #{value} was not " \
226
+ "applied. React Native has probably renamed it; this needs updating " \
227
+ "in scripts/react_native_quickjs_pods.rb."
228
+ end
229
+
230
+ result.native_target.build_configurations.each do |config|
231
+ next if debug_only && config.type != :debug
232
+
233
+ current = config.build_settings[setting] || "$(inherited)"
234
+ current = current.join(" ") if current.is_a?(Array)
235
+ config.build_settings[setting] = "#{current} #{value}"
236
+ end
237
+ end