@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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/NOTICE +31 -0
- package/README.md +217 -0
- package/ReactNativeQuickJS.podspec +197 -0
- package/android/CMakeLists.txt +134 -0
- package/android/build.gradle +214 -0
- package/android/quickjs.gradle +39 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/reactnativequickjs/quickjs/QuickJSInstance.kt +39 -0
- package/android/src/main/java/com/reactnativequickjs/quickjs/QuickJSPackage.kt +32 -0
- package/android/src/main/jni/JJSRuntimeFactory.h +37 -0
- package/android/src/main/jni/JQuickJSInstance.cpp +27 -0
- package/android/src/main/jni/JQuickJSInstance.h +46 -0
- package/android/src/main/jni/OnLoad.cpp +15 -0
- package/app.plugin.js +11 -0
- package/apple/RCTQuickJSInstanceFactory.h +32 -0
- package/apple/RCTQuickJSInstanceFactory.mm +16 -0
- package/bin/qjsc/darwin-arm64/qjsc +0 -0
- package/bin/qjsc/darwin-x64/qjsc +0 -0
- package/bin/qjsc/linux-arm64/qjsc +0 -0
- package/bin/qjsc/linux-x64/qjsc +0 -0
- package/bin/qjsc/manifest.json +11 -0
- package/bin/qjsc/win32-x64/qjsc.exe +0 -0
- package/bin/react-native-quickjs.js +28 -0
- package/cmake/jsi.cmake +24 -0
- package/cmake/quickjs.cmake +125 -0
- package/engine/quickjs-rel/LICENSE +24 -0
- package/engine/quickjs-rel/MANIFEST.json +55 -0
- package/engine/quickjs-rel/builtin-array-fromasync.h +120 -0
- package/engine/quickjs-rel/builtin-iterator-zip-keyed.h +332 -0
- package/engine/quickjs-rel/builtin-iterator-zip.h +337 -0
- package/engine/quickjs-rel/cutils.h +1998 -0
- package/engine/quickjs-rel/dtoa.c +1619 -0
- package/engine/quickjs-rel/dtoa.h +87 -0
- package/engine/quickjs-rel/libregexp-opcode.h +73 -0
- package/engine/quickjs-rel/libregexp.c +3478 -0
- package/engine/quickjs-rel/libregexp.h +101 -0
- package/engine/quickjs-rel/libunicode-table.h +5173 -0
- package/engine/quickjs-rel/libunicode.c +2069 -0
- package/engine/quickjs-rel/libunicode.h +172 -0
- package/engine/quickjs-rel/list.h +107 -0
- package/engine/quickjs-rel/quickjs-atom.h +280 -0
- package/engine/quickjs-rel/quickjs-c-atomics.h +54 -0
- package/engine/quickjs-rel/quickjs-opcode.h +385 -0
- package/engine/quickjs-rel/quickjs.c +64854 -0
- package/engine/quickjs-rel/quickjs.h +1577 -0
- package/modules/cdp/quickjs-cdp.c +1386 -0
- package/modules/cdp/quickjs-cdp.h +122 -0
- package/modules/cdp/react/QuickJSInspector.cpp +322 -0
- package/modules/cdp/react/QuickJSInspector.h +114 -0
- package/modules/hermes-compat/include/hermes/DebuggerAPI.h +20 -0
- package/modules/hermes-compat/include/hermes/Public/CrashManager.h +47 -0
- package/modules/hermes-compat/include/hermes/Public/CtorConfig.h +88 -0
- package/modules/hermes-compat/include/hermes/Public/GCConfig.h +83 -0
- package/modules/hermes-compat/include/hermes/Public/HermesExport.h +17 -0
- package/modules/hermes-compat/include/hermes/Public/RuntimeConfig.h +66 -0
- package/modules/hermes-compat/include/hermes/Public/SamplingProfiler.h +22 -0
- package/modules/hermes-compat/include/hermes/hermes.h +195 -0
- package/modules/hermes-compat/include/hermes/inspector/RuntimeAdapter.h +43 -0
- package/modules/hermes-compat/include/hermes/inspector-modern/chrome/Registration.h +31 -0
- package/modules/hermes-compat/include/hermes-compat/Diagnostics.h +35 -0
- package/modules/hermes-compat/src/HermesCompat.cpp +557 -0
- package/package.json +104 -0
- package/scripts/bytecode/compile.js +99 -0
- package/scripts/expo/plugin.js +231 -0
- package/scripts/postinstall.js +74 -0
- package/scripts/react_native_quickjs_pods.rb +237 -0
- package/scripts/setup/edits.js +311 -0
- package/scripts/setup/run.js +121 -0
- package/src/bytecode/QuickJSBytecode.cpp +50 -0
- package/src/bytecode/QuickJSBytecode.h +50 -0
- package/src/module/QuickJSCompat.cpp +92 -0
- package/src/module/QuickJSCompat.h +19 -0
- package/src/module/QuickJSModule.cpp +70 -0
- package/src/module/QuickJSModule.h +108 -0
- package/src/module/QuickJSModuleNative.h +93 -0
- package/src/runtime/QuickJSInstance.cpp +37 -0
- package/src/runtime/QuickJSInstance.h +74 -0
- package/src/runtime/QuickJSRuntime.cpp +1864 -0
- package/src/runtime/QuickJSRuntime.h +529 -0
- package/src/runtime/QuickJSRuntimeConfig.h +103 -0
- package/src/runtime/QuickJSRuntimeFactory.cpp +34 -0
- 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
|