@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,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