@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,311 @@
|
|
|
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 build-config edits that move an app between Hermes and QuickJS, written
|
|
8
|
+
* once. `install` applies them, `revert` undoes them, `doctor` reports them,
|
|
9
|
+
* and the Expo plugin runs the same functions inside Expo's own mods.
|
|
10
|
+
*
|
|
11
|
+
* Each edit is:
|
|
12
|
+
*
|
|
13
|
+
* label what to call the file in output
|
|
14
|
+
* findFile absolute path in a project, or null if the project has none
|
|
15
|
+
* isApplied is this file already configured for QuickJS?
|
|
16
|
+
* addQuickJS returns the edited source, or null (see below)
|
|
17
|
+
* removeQuickJS the inverse
|
|
18
|
+
* manualSteps what to tell someone when the functions return null
|
|
19
|
+
*
|
|
20
|
+
* Returning null means "this file does not look the way I expect". That is not
|
|
21
|
+
* an error to throw -- the file has been customised, and the caller prints
|
|
22
|
+
* manualSteps instead of guessing.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
const PACKAGE = '@react-native-quickjs/quickjs';
|
|
31
|
+
const KOTLIN_IMPORT = 'import com.reactnativequickjs.quickjs.QuickJSInstance';
|
|
32
|
+
const SWIFT_IMPORT = 'import ReactNativeQuickJS';
|
|
33
|
+
const PODFILE_REQUIRE = `require_relative '../node_modules/${PACKAGE}/scripts/react_native_quickjs_pods.rb'`;
|
|
34
|
+
const GRADLE_APPLY = `apply from: file("../../node_modules/${PACKAGE}/android/quickjs.gradle")`;
|
|
35
|
+
|
|
36
|
+
const SKIP_DIRECTORIES = new Set(['build', 'node_modules', 'Pods']);
|
|
37
|
+
|
|
38
|
+
/** The first file called `fileName` anywhere under `directory`. */
|
|
39
|
+
function findFileNamed(directory, fileName, depth = 7) {
|
|
40
|
+
if (depth < 0 || !fs.existsSync(directory)) return null;
|
|
41
|
+
|
|
42
|
+
const subdirectories = [];
|
|
43
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
44
|
+
if (SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
45
|
+
const fullPath = path.join(directory, entry.name);
|
|
46
|
+
if (entry.isDirectory()) subdirectories.push(fullPath);
|
|
47
|
+
else if (entry.name === fileName) return fullPath;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const subdirectory of subdirectories) {
|
|
51
|
+
const found = findFileNamed(subdirectory, fileName, depth - 1);
|
|
52
|
+
if (found) return found;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Index of the `)` that closes the call whose `(` is at `openParen`. */
|
|
58
|
+
function closingParenIndex(source, openParen) {
|
|
59
|
+
let depth = 0;
|
|
60
|
+
for (let i = openParen; i < source.length; i++) {
|
|
61
|
+
if (source[i] === '(') depth++;
|
|
62
|
+
else if (source[i] === ')' && --depth === 0) return i;
|
|
63
|
+
}
|
|
64
|
+
return -1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The source with `importLine` added after the last import, or null. */
|
|
68
|
+
function withImportAdded(source, importLine) {
|
|
69
|
+
if (source.includes(importLine)) return source;
|
|
70
|
+
|
|
71
|
+
const imports = [...source.matchAll(/^import .*$/gm)];
|
|
72
|
+
if (imports.length === 0) return null;
|
|
73
|
+
|
|
74
|
+
const lastImport = imports[imports.length - 1];
|
|
75
|
+
const insertAt = lastImport.index + lastImport[0].length;
|
|
76
|
+
return source.slice(0, insertAt) + '\n' + importLine + source.slice(insertAt);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The source without `line`, and without the blank line that followed it.
|
|
81
|
+
* A plain string match, so there is no regular expression to escape.
|
|
82
|
+
*/
|
|
83
|
+
function withLineRemoved(source, line) {
|
|
84
|
+
return source.includes(line + '\n\n')
|
|
85
|
+
? source.replace(line + '\n\n', '')
|
|
86
|
+
: source.replace(line + '\n', '');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The indentation of the first line at or after `index`. */
|
|
90
|
+
function indentAt(source, index, fallback) {
|
|
91
|
+
const match = /^([ \t]+)\S/.exec(source.slice(index));
|
|
92
|
+
return match ? match[1] : fallback;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const edits = [
|
|
96
|
+
{
|
|
97
|
+
label: 'android/gradle.properties',
|
|
98
|
+
findFile: (project) => path.join(project, 'android', 'gradle.properties'),
|
|
99
|
+
isApplied: (source) => /^[ \t]*hermesEnabled[ \t]*=[ \t]*false[ \t]*$/m.test(source),
|
|
100
|
+
|
|
101
|
+
addQuickJS: (source) =>
|
|
102
|
+
/^[ \t]*hermesEnabled[ \t]*=/m.test(source)
|
|
103
|
+
? source.replace(/^([ \t]*hermesEnabled[ \t]*=[ \t]*).*$/m, '$1false')
|
|
104
|
+
: `${source.replace(/\s*$/, '')}\n\nhermesEnabled=false\n`,
|
|
105
|
+
|
|
106
|
+
removeQuickJS: (source) =>
|
|
107
|
+
source.replace(/^([ \t]*hermesEnabled[ \t]*=[ \t]*).*$/m, '$1true'),
|
|
108
|
+
|
|
109
|
+
manualSteps: ['Set hermesEnabled=false in android/gradle.properties'],
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
// The template picks Hermes or JavaScriptCore here, and hermesEnabled=false
|
|
114
|
+
// is what sends it down the JavaScriptCore branch. An app on QuickJS wants
|
|
115
|
+
// neither, so the whole block goes.
|
|
116
|
+
label: 'android/app/build.gradle',
|
|
117
|
+
findFile: (project) => path.join(project, 'android', 'app', 'build.gradle'),
|
|
118
|
+
isApplied: (source) =>
|
|
119
|
+
!/hermes-android|implementation jscFlavor/.test(source) && source.includes(GRADLE_APPLY),
|
|
120
|
+
|
|
121
|
+
// `def jscFlavor` is left alone. It declares the coordinate; the dependency
|
|
122
|
+
// this removes is what pulled libjsc.so into the app, and leaving the
|
|
123
|
+
// declaration keeps revert able to put the block back unchanged.
|
|
124
|
+
addQuickJS(source) {
|
|
125
|
+
const withoutEngines = source.replace(
|
|
126
|
+
/[ \t]*if \([ \t]*hermesEnabled\.toBoolean\(\)[ \t]*\) \{[\s\S]*?\n[ \t]*\}[ \t]*\n/,
|
|
127
|
+
''
|
|
128
|
+
);
|
|
129
|
+
// The block being absent is fine on its own -- a rerun, or an app that
|
|
130
|
+
// never had it. Absent while the dependencies are still declared means
|
|
131
|
+
// the file has been rewritten into a shape this does not recognise.
|
|
132
|
+
if (withoutEngines === source && /hermes-android|implementation jscFlavor/.test(source)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Appended, not inserted: quickjs.gradle configures the `react` extension
|
|
137
|
+
// and the app's variants, so it has to run after the plugins that create
|
|
138
|
+
// them, and the end of the file is the only place guaranteed to be after
|
|
139
|
+
// all of them.
|
|
140
|
+
return withoutEngines.includes(GRADLE_APPLY)
|
|
141
|
+
? withoutEngines
|
|
142
|
+
: `${withoutEngines.replace(/\s*$/, '')}\n\n${GRADLE_APPLY}\n`;
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
removeQuickJS(source) {
|
|
146
|
+
const reactAndroid = /([ \t]*)implementation\("com\.facebook\.react:react-android"\)\n/;
|
|
147
|
+
if (!reactAndroid.test(source)) return null;
|
|
148
|
+
|
|
149
|
+
// The apply line is the last line of the file, so removing it leaves the
|
|
150
|
+
// blank line that separated it behind.
|
|
151
|
+
const withoutApply = withLineRemoved(source, GRADLE_APPLY).replace(/\n+$/, '\n');
|
|
152
|
+
|
|
153
|
+
return withoutApply.replace(
|
|
154
|
+
reactAndroid,
|
|
155
|
+
(line, indent) =>
|
|
156
|
+
`${line}\n${indent}if (hermesEnabled.toBoolean()) {\n` +
|
|
157
|
+
`${indent} implementation("com.facebook.react:hermes-android")\n` +
|
|
158
|
+
`${indent}} else {\n` +
|
|
159
|
+
`${indent} implementation jscFlavor\n` +
|
|
160
|
+
`${indent}}\n`
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
manualSteps: [
|
|
165
|
+
'In android/app/build.gradle, delete the dependencies block that picks',
|
|
166
|
+
'between com.facebook.react:hermes-android and jscFlavor, and add this',
|
|
167
|
+
'as the last line of the file:',
|
|
168
|
+
` ${GRADLE_APPLY}`,
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
label: 'MainApplication.kt',
|
|
174
|
+
findFile: (project) =>
|
|
175
|
+
findFileNamed(path.join(project, 'android', 'app', 'src'), 'MainApplication.kt'),
|
|
176
|
+
isApplied: (source) => source.includes('QuickJSInstance('),
|
|
177
|
+
|
|
178
|
+
addQuickJS(source) {
|
|
179
|
+
const withImport = withImportAdded(source, KOTLIN_IMPORT);
|
|
180
|
+
if (!withImport) return null;
|
|
181
|
+
|
|
182
|
+
const call = /getDefaultReactHost\(\n/.exec(withImport);
|
|
183
|
+
if (!call) return null;
|
|
184
|
+
|
|
185
|
+
// Added as the first argument rather than the last. Kotlin named
|
|
186
|
+
// arguments may be given in any order, and the first one's indentation
|
|
187
|
+
// is the only one readable without matching parens through the nested
|
|
188
|
+
// PackageList lambda.
|
|
189
|
+
const firstArgument = call.index + call[0].length;
|
|
190
|
+
const indent = indentAt(withImport, firstArgument, ' ');
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
withImport.slice(0, firstArgument) +
|
|
194
|
+
`${indent}jsRuntimeFactory = QuickJSInstance(),\n` +
|
|
195
|
+
withImport.slice(firstArgument)
|
|
196
|
+
);
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
removeQuickJS: (source) =>
|
|
200
|
+
withLineRemoved(source, KOTLIN_IMPORT).replace(
|
|
201
|
+
/^[ \t]*jsRuntimeFactory = QuickJSInstance\(\),?[ \t]*\n/m,
|
|
202
|
+
''
|
|
203
|
+
),
|
|
204
|
+
|
|
205
|
+
manualSteps: [
|
|
206
|
+
'In MainApplication.kt add:',
|
|
207
|
+
` ${KOTLIN_IMPORT}`,
|
|
208
|
+
'and pass jsRuntimeFactory = QuickJSInstance() to getDefaultReactHost().',
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
{
|
|
213
|
+
label: 'ios/Podfile',
|
|
214
|
+
findFile: (project) => path.join(project, 'ios', 'Podfile'),
|
|
215
|
+
isApplied: (source) => source.includes('use_quickjs!'),
|
|
216
|
+
|
|
217
|
+
addQuickJS(source) {
|
|
218
|
+
// Every anchor is checked before anything is written, so a Podfile this
|
|
219
|
+
// does not recognise is left alone rather than half configured.
|
|
220
|
+
const hasPlatform = /^platform :ios/m.test(source);
|
|
221
|
+
const useReactNative = /^([ \t]*)use_react_native!\(/m.exec(source);
|
|
222
|
+
const postInstall = source.indexOf('react_native_post_install(');
|
|
223
|
+
if (!hasPlatform || !useReactNative || postInstall === -1) return null;
|
|
224
|
+
|
|
225
|
+
const withRequire = source.includes(PODFILE_REQUIRE)
|
|
226
|
+
? source
|
|
227
|
+
: source.replace(/^platform :ios/m, `${PODFILE_REQUIRE}\n\nplatform :ios`);
|
|
228
|
+
|
|
229
|
+
// use_quickjs! only sets environment flags, but everything after it reads
|
|
230
|
+
// them. use_native_modules! evaluates every autolinked podspec, and
|
|
231
|
+
// use_expo_modules! resolves Expo's modules against the prebuilt React
|
|
232
|
+
// Native this is about to turn off -- both of which run before
|
|
233
|
+
// use_react_native!. So it goes before whichever comes first.
|
|
234
|
+
const anchor =
|
|
235
|
+
/^([ \t]*)(use_native_modules!|use_expo_modules!|use_react_native!\()/m.exec(withRequire);
|
|
236
|
+
if (!anchor) return null;
|
|
237
|
+
|
|
238
|
+
const withUseQuickJS =
|
|
239
|
+
withRequire.slice(0, anchor.index) +
|
|
240
|
+
`${anchor[1]}use_quickjs!\n\n` +
|
|
241
|
+
withRequire.slice(anchor.index);
|
|
242
|
+
|
|
243
|
+
// The hook must follow React Native's own, which writes the USE_HERMES
|
|
244
|
+
// build setting this overwrites.
|
|
245
|
+
const call = withUseQuickJS.indexOf('react_native_post_install(');
|
|
246
|
+
const closing = closingParenIndex(withUseQuickJS, withUseQuickJS.indexOf('(', call));
|
|
247
|
+
if (closing === -1) return null;
|
|
248
|
+
|
|
249
|
+
const endOfLine = withUseQuickJS.indexOf('\n', closing);
|
|
250
|
+
const indent = /\n([ \t]*)$/.exec(withUseQuickJS.slice(0, call));
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
withUseQuickJS.slice(0, endOfLine + 1) +
|
|
254
|
+
`\n${indent ? indent[1] : ' '}react_native_quickjs_post_install(installer)\n` +
|
|
255
|
+
withUseQuickJS.slice(endOfLine + 1)
|
|
256
|
+
);
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
removeQuickJS: (source) =>
|
|
260
|
+
withLineRemoved(source, PODFILE_REQUIRE)
|
|
261
|
+
.replace(/^[ \t]*use_quickjs!\n\n?/m, '')
|
|
262
|
+
.replace(/\n?[ \t]*react_native_quickjs_post_install\(installer\)\n/, '\n'),
|
|
263
|
+
|
|
264
|
+
manualSteps: [
|
|
265
|
+
'In ios/Podfile add, before use_expo_modules! or use_react_native!:',
|
|
266
|
+
` ${PODFILE_REQUIRE}`,
|
|
267
|
+
' use_quickjs!',
|
|
268
|
+
'and react_native_quickjs_post_install(installer) after react_native_post_install.',
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
{
|
|
273
|
+
label: 'AppDelegate.swift',
|
|
274
|
+
findFile: (project) => findFileNamed(path.join(project, 'ios'), 'AppDelegate.swift'),
|
|
275
|
+
isApplied: (source) => source.includes('jsrt_create_quickjs_factory'),
|
|
276
|
+
|
|
277
|
+
addQuickJS(source) {
|
|
278
|
+
const withImport = withImportAdded(source, SWIFT_IMPORT);
|
|
279
|
+
if (!withImport) return null;
|
|
280
|
+
|
|
281
|
+
// Expo names its own subclass ExpoReactNativeFactoryDelegate, so the
|
|
282
|
+
// class this overrides is matched by suffix rather than by exact name.
|
|
283
|
+
const delegateClass =
|
|
284
|
+
/class\s+\w+\s*:\s*\w*ReactNativeFactoryDelegate\s*\{/.exec(withImport);
|
|
285
|
+
if (!delegateClass) return null;
|
|
286
|
+
|
|
287
|
+
const classBody = delegateClass.index + delegateClass[0].length;
|
|
288
|
+
return (
|
|
289
|
+
withImport.slice(0, classBody) +
|
|
290
|
+
'\n override func createJSRuntimeFactory() -> JSRuntimeFactoryRef {\n' +
|
|
291
|
+
' jsrt_create_quickjs_factory()\n' +
|
|
292
|
+
' }\n' +
|
|
293
|
+
withImport.slice(classBody)
|
|
294
|
+
);
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
removeQuickJS: (source) =>
|
|
298
|
+
withLineRemoved(source, SWIFT_IMPORT).replace(
|
|
299
|
+
/\n[ \t]*override func createJSRuntimeFactory\(\)[^\n]*\{\n[ \t]*jsrt_create_quickjs_factory\(\)\n[ \t]*\}\n/,
|
|
300
|
+
'\n'
|
|
301
|
+
),
|
|
302
|
+
|
|
303
|
+
manualSteps: [
|
|
304
|
+
'In AppDelegate.swift add:',
|
|
305
|
+
` ${SWIFT_IMPORT}`,
|
|
306
|
+
'and override createJSRuntimeFactory() to return jsrt_create_quickjs_factory().',
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
module.exports = { edits };
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
* install, revert and doctor. All three walk the same table in edits.js.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { edits } = require('./edits');
|
|
15
|
+
|
|
16
|
+
const GREEN = '\x1b[32m';
|
|
17
|
+
const YELLOW = '\x1b[33m';
|
|
18
|
+
const RED = '\x1b[31m';
|
|
19
|
+
const DIM = '\x1b[2m';
|
|
20
|
+
const OFF = '\x1b[0m';
|
|
21
|
+
|
|
22
|
+
function resolveProjectDirectory(argv) {
|
|
23
|
+
const at = argv.indexOf('--project');
|
|
24
|
+
const dir = at === -1 ? process.cwd() : path.resolve(argv[at + 1] || '.');
|
|
25
|
+
if (!fs.existsSync(path.join(dir, 'package.json'))) {
|
|
26
|
+
console.error(`\nNo package.json in ${dir}. Run this from a React Native app.\n`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {'addQuickJS'|'removeQuickJS'} direction
|
|
34
|
+
* @returns {number} exit code
|
|
35
|
+
*/
|
|
36
|
+
function applyEdits(direction, argv) {
|
|
37
|
+
const project = resolveProjectDirectory(argv);
|
|
38
|
+
const dryRun = argv.includes('--dry-run');
|
|
39
|
+
const manual = [];
|
|
40
|
+
let changed = 0;
|
|
41
|
+
|
|
42
|
+
console.log('');
|
|
43
|
+
for (const edit of edits) {
|
|
44
|
+
const file = edit.findFile(project);
|
|
45
|
+
if (!file || !fs.existsSync(file)) {
|
|
46
|
+
console.log(` ${DIM}skipped${OFF} ${edit.label} ${DIM}(not in this project)${OFF}`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const before = fs.readFileSync(file, 'utf8');
|
|
51
|
+
const satisfied = direction === 'addQuickJS' ? edit.isApplied(before) : !edit.isApplied(before);
|
|
52
|
+
if (satisfied) {
|
|
53
|
+
console.log(` ${DIM}already${OFF} ${edit.label}`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const after = edit[direction](before);
|
|
58
|
+
if (after == null || after === before) {
|
|
59
|
+
console.log(` ${YELLOW}by hand${OFF} ${edit.label}`);
|
|
60
|
+
manual.push([edit.label, edit.manualSteps]);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!dryRun) fs.writeFileSync(file, after);
|
|
65
|
+
console.log(` ${GREEN}${dryRun ? 'would' : 'wrote'}${OFF} ${edit.label}`);
|
|
66
|
+
changed++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const [label, lines] of manual) {
|
|
70
|
+
console.log(`\n${YELLOW}${label}${OFF} could not be edited automatically:`);
|
|
71
|
+
for (const line of lines) console.log(` ${line}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (changed && !dryRun && direction === 'addQuickJS') {
|
|
75
|
+
console.log(`\nNext: cd ios && pod install\n`);
|
|
76
|
+
} else {
|
|
77
|
+
console.log('');
|
|
78
|
+
}
|
|
79
|
+
return manual.length ? 1 : 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function doctor(argv) {
|
|
83
|
+
const project = resolveProjectDirectory(argv);
|
|
84
|
+
let notConfigured = 0;
|
|
85
|
+
let found = 0;
|
|
86
|
+
|
|
87
|
+
console.log('');
|
|
88
|
+
for (const edit of edits) {
|
|
89
|
+
const file = edit.findFile(project);
|
|
90
|
+
if (!file || !fs.existsSync(file)) {
|
|
91
|
+
console.log(` ${DIM}—${OFF} ${edit.label} ${DIM}(not in this project)${OFF}`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
found++;
|
|
95
|
+
if (edit.isApplied(fs.readFileSync(file, 'utf8'))) {
|
|
96
|
+
console.log(` ${GREEN}✓${OFF} ${edit.label}`);
|
|
97
|
+
} else {
|
|
98
|
+
console.log(` ${RED}✗${OFF} ${edit.label}`);
|
|
99
|
+
notConfigured++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!found) {
|
|
104
|
+
console.log(`\nNo React Native app found in ${project}\nUse --project <path> to point at one.\n`);
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(
|
|
109
|
+
notConfigured
|
|
110
|
+
? `\n${notConfigured} of ${edits.length} not configured for QuickJS. ` +
|
|
111
|
+
`Run: npx react-native-quickjs install\n`
|
|
112
|
+
: `\nConfigured to run on QuickJS.\n`
|
|
113
|
+
);
|
|
114
|
+
return notConfigured ? 1 : 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
install: (argv) => applyEdits('addQuickJS', argv),
|
|
119
|
+
revert: (argv) => applyEdits('removeQuickJS', argv),
|
|
120
|
+
doctor,
|
|
121
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
|
|
8
|
+
#include "QuickJSBytecode.h"
|
|
9
|
+
|
|
10
|
+
#include <cstring>
|
|
11
|
+
|
|
12
|
+
namespace qjs {
|
|
13
|
+
|
|
14
|
+
uint32_t bytecodeFormatVersion(const uint8_t *data) {
|
|
15
|
+
const uint8_t *version = data + kBytecodeMagicSize;
|
|
16
|
+
return static_cast<uint32_t>(version[0]) |
|
|
17
|
+
(static_cast<uint32_t>(version[1]) << 8) |
|
|
18
|
+
(static_cast<uint32_t>(version[2]) << 16) |
|
|
19
|
+
(static_cast<uint32_t>(version[3]) << 24);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
bool isBytecodeContainer(const uint8_t *data, size_t size) {
|
|
23
|
+
if (data == nullptr || size < kBytecodeHeaderSize) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (std::memcmp(data, kBytecodeMagic, kBytecodeMagicSize) != 0) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return bytecodeFormatVersion(data) == kBytecodeFormatVersion;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
bool isHermesBytecode(const uint8_t *data, size_t size) {
|
|
33
|
+
// hermes::hbc::MAGIC, from BCGen/HBC/BytecodeFileFormat.h. Read a byte at a
|
|
34
|
+
// time: the buffer has no alignment guarantee.
|
|
35
|
+
static constexpr uint64_t kHermesMagic = 0x1F1903C103BC1FC6ULL;
|
|
36
|
+
|
|
37
|
+
if (data == nullptr || size < sizeof(uint64_t)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
uint64_t magic = 0;
|
|
42
|
+
for (size_t i = 0; i < sizeof(uint64_t); i++) {
|
|
43
|
+
magic |= static_cast<uint64_t>(data[i]) << (8 * i);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ~MAGIC marks a delta bundle, equally unexecutable here.
|
|
47
|
+
return magic == kHermesMagic || magic == ~kHermesMagic;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
} // namespace qjs
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include <cstddef>
|
|
11
|
+
#include <cstdint>
|
|
12
|
+
|
|
13
|
+
namespace qjs {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Precompiled bytecode container, written by tools/bytecode/qjsc.c.
|
|
17
|
+
*
|
|
18
|
+
* [8 bytes magic "NSBCNGS\0"][4 bytes format version, little-endian]
|
|
19
|
+
* [JS_WriteObject payload]
|
|
20
|
+
*
|
|
21
|
+
* A JS_WriteObject payload has no strong leading magic of its own, so it is
|
|
22
|
+
* wrapped to make detection unambiguous. The magic is specific to quickjs-ng;
|
|
23
|
+
* bytecode from Bellard's quickjs is not interchangeable.
|
|
24
|
+
*/
|
|
25
|
+
inline constexpr char kBytecodeMagic[8] = {'N', 'S', 'B', 'C',
|
|
26
|
+
'N', 'G', 'S', '\0'};
|
|
27
|
+
inline constexpr size_t kBytecodeMagicSize = sizeof(kBytecodeMagic);
|
|
28
|
+
inline constexpr size_t kBytecodeHeaderSize = kBytecodeMagicSize + 4;
|
|
29
|
+
inline constexpr uint32_t kBytecodeFormatVersion = 1;
|
|
30
|
+
|
|
31
|
+
/// True if `data` opens a container this runtime can execute. An unrecognised
|
|
32
|
+
/// format version reads as false, so a stale artifact is treated as source
|
|
33
|
+
/// rather than mis-parsed as bytecode.
|
|
34
|
+
bool isBytecodeContainer(const uint8_t *data, size_t size);
|
|
35
|
+
|
|
36
|
+
/// Only meaningful when `size >= kBytecodeHeaderSize`.
|
|
37
|
+
uint32_t bytecodeFormatVersion(const uint8_t *data);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* True if `data` is a Hermes bytecode bundle.
|
|
41
|
+
*
|
|
42
|
+
* Detected only to name it. An app that sets `hermesEnabled=true` in Gradle to
|
|
43
|
+
* satisfy a library that expects Hermes also makes `BundleHermesCTask` run
|
|
44
|
+
* `hermesc`, so the shipped bundle becomes Hermes bytecode. Unrecognised, it
|
|
45
|
+
* falls through to the parser as binary and surfaces as an unintelligible
|
|
46
|
+
* syntax error at byte zero.
|
|
47
|
+
*/
|
|
48
|
+
bool isHermesBytecode(const uint8_t *data, size_t size);
|
|
49
|
+
|
|
50
|
+
} // namespace qjs
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
|
|
8
|
+
#include "QuickJSCompat.h"
|
|
9
|
+
|
|
10
|
+
namespace qjs {
|
|
11
|
+
|
|
12
|
+
namespace jsi = facebook::jsi;
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* React Native detects engine capabilities by looking for
|
|
16
|
+
* `global.HermesInternal`. That is not a capability protocol -- it is a check
|
|
17
|
+
* for one specific engine -- so every other engine silently gets the fallback
|
|
18
|
+
* path. There are three uses in React Native 0.85:
|
|
19
|
+
*
|
|
20
|
+
* Libraries/Core/polyfillPromise.js:25
|
|
21
|
+
* if (global?.HermesInternal?.hasPromise?.()) { native Promise }
|
|
22
|
+
* else { polyfillGlobal('Promise', ...) }
|
|
23
|
+
* Libraries/Core/polyfillPromise.js:32
|
|
24
|
+
* enablePromiseRejectionTracker(...), in __DEV__ only
|
|
25
|
+
* Libraries/Core/Devtools/parseErrorStack.js:51
|
|
26
|
+
* branches on it to choose a stack-trace parser
|
|
27
|
+
*
|
|
28
|
+
* Without this shim every app replaces quickjs's native Promise with
|
|
29
|
+
* `promise@7.3.1`, a JavaScript implementation on top of setImmediate.
|
|
30
|
+
* Measured here: constructing and chaining costs 1205 ns through the polyfill
|
|
31
|
+
* against 775 ns native, and every `await` in the app pays it.
|
|
32
|
+
*
|
|
33
|
+
* The third use is the one that could bite, since declaring the object reroutes
|
|
34
|
+
* stack parsing to parseHermesStack. Its frame pattern is
|
|
35
|
+
*
|
|
36
|
+
* /^ {4}at (.+?)(?: \((native)\)?| \((address at )?(.*?):(\d+):(\d+)\))$/
|
|
37
|
+
*
|
|
38
|
+
* and quickjs emits exactly that shape, ` at inner (/path/file.js:1:29)`,
|
|
39
|
+
* including the `(native)` form. Checked against real quickjs stacks: every
|
|
40
|
+
* frame parses with the captures the Hermes parser expects.
|
|
41
|
+
*
|
|
42
|
+
* The honest cost is outside React Native core, where third-party libraries use
|
|
43
|
+
* the same object as an "am I on Hermes" test, sometimes to work around a
|
|
44
|
+
* Hermes bug we do not have. That is why this is a config flag rather than
|
|
45
|
+
* unconditional, and why getRuntimeProperties() below tells the truth about
|
|
46
|
+
* which engine this is.
|
|
47
|
+
*/
|
|
48
|
+
void installReactNativeCompat(jsi::Runtime &runtime) {
|
|
49
|
+
auto hermesInternal = jsi::Object(runtime);
|
|
50
|
+
|
|
51
|
+
hermesInternal.setProperty(
|
|
52
|
+
runtime, "hasPromise",
|
|
53
|
+
jsi::Function::createFromHostFunction(
|
|
54
|
+
runtime, jsi::PropNameID::forAscii(runtime, "hasPromise"), 0,
|
|
55
|
+
[](jsi::Runtime &, const jsi::Value &, const jsi::Value *, size_t) {
|
|
56
|
+
return jsi::Value(true);
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
// Accepting and ignoring the options is deliberate: unhandled-rejection
|
|
60
|
+
// reporting belongs to the host, which installs its own tracker through
|
|
61
|
+
// JS_SetHostPromiseRejectionTracker. Throwing "not implemented" here would
|
|
62
|
+
// break every development build for a feature the host already owns.
|
|
63
|
+
hermesInternal.setProperty(
|
|
64
|
+
runtime, "enablePromiseRejectionTracker",
|
|
65
|
+
jsi::Function::createFromHostFunction(
|
|
66
|
+
runtime,
|
|
67
|
+
jsi::PropNameID::forAscii(runtime, "enablePromiseRejectionTracker"),
|
|
68
|
+
1,
|
|
69
|
+
[](jsi::Runtime &, const jsi::Value &, const jsi::Value *, size_t) {
|
|
70
|
+
return jsi::Value::undefined();
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
hermesInternal.setProperty(
|
|
74
|
+
runtime, "getRuntimeProperties",
|
|
75
|
+
jsi::Function::createFromHostFunction(
|
|
76
|
+
runtime, jsi::PropNameID::forAscii(runtime, "getRuntimeProperties"),
|
|
77
|
+
0,
|
|
78
|
+
[](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) {
|
|
79
|
+
auto props = jsi::Object(rt);
|
|
80
|
+
props.setProperty(
|
|
81
|
+
rt, "OSS Release Version",
|
|
82
|
+
jsi::String::createFromAscii(rt, "quickjs-ng"));
|
|
83
|
+
props.setProperty(
|
|
84
|
+
rt, "Engine", jsi::String::createFromAscii(rt, "QuickJS"));
|
|
85
|
+
props.setProperty(rt, "isHermes", jsi::Value(false));
|
|
86
|
+
return jsi::Value(std::move(props));
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
runtime.global().setProperty(runtime, "HermesInternal", hermesInternal);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
} // namespace qjs
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include <jsi/jsi.h>
|
|
11
|
+
|
|
12
|
+
namespace qjs {
|
|
13
|
+
|
|
14
|
+
/// Installs the `HermesInternal` object React Native probes for. See the
|
|
15
|
+
/// implementation for what it costs to omit and why claiming the name is safe.
|
|
16
|
+
/// Controlled by QuickJSRuntimeConfig::reactNativeCompat.
|
|
17
|
+
void installReactNativeCompat(facebook::jsi::Runtime &runtime);
|
|
18
|
+
|
|
19
|
+
} // namespace qjs
|