@react-native-quickjs/quickjs 1.0.0-alpha.0 → 1.0.0-alpha.2
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 +2 -2
- package/README.md +8 -0
- package/android/CMakeLists.txt +87 -15
- package/android/build.gradle +31 -7
- package/engine/quickjs-rel/MANIFEST.json +5 -1
- package/engine/quickjs-rel/quickjs.c +10 -1
- package/package.json +7 -4
- package/scripts/generate-module-registry.js +234 -0
- package/scripts/react_native_quickjs_pods.rb +130 -0
- package/src/module/QuickJSModule.cpp +9 -0
- package/src/module/QuickJSModule.h +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -29,8 +29,8 @@ Nothing has been published yet. This section becomes the first release entry.
|
|
|
29
29
|
- React Native's non-Hermes path is patched at `pod install`. Each workaround
|
|
30
30
|
fails with a named error if React Native changes.
|
|
31
31
|
- Expo's `getDefaultReactHost` hardcodes Hermes, so the config plugin edits
|
|
32
|
-
`node_modules`. Until [expo#
|
|
32
|
+
`node_modules`. Until [expo#49686][expo-pr] lands, an `npm install` reverts
|
|
33
33
|
that edit and the app silently launches on Hermes.
|
|
34
34
|
|
|
35
35
|
[ng]: https://github.com/quickjs-ng/quickjs
|
|
36
|
-
[expo-pr]: https://github.com/expo/expo/
|
|
36
|
+
[expo-pr]: https://github.com/expo/expo/pull/49686
|
package/README.md
CHANGED
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
React Native, compiled from source in every app.
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="https://www.npmjs.com/package/@react-native-quickjs/quickjs"><img
|
|
20
|
+
alt="@react-native-quickjs/quickjs on npm"
|
|
21
|
+
src="https://img.shields.io/npm/v/@react-native-quickjs/quickjs/alpha?color=cb3837"></a>
|
|
22
|
+
<a href="LICENSE"><img alt="MIT licensed"
|
|
23
|
+
src="https://img.shields.io/badge/license-MIT-blue"></a>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
18
26
|
Provides a `jsi::Runtime` backed by quickjs-ng, and the `JSRuntimeFactory` glue
|
|
19
27
|
React Native uses to select it. The engine is compiled from source as part of
|
|
20
28
|
the app build. Neither Hermes nor JavaScriptCore is linked into the result.
|
package/android/CMakeLists.txt
CHANGED
|
@@ -25,10 +25,60 @@ add_compile_options(${folly_FLAGS})
|
|
|
25
25
|
|
|
26
26
|
include("${RNQJS_ROOT}/cmake/quickjs.cmake")
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
# Before any module subdirectory is configured: each module's CMakeLists probes
|
|
29
|
+
# for ReactAndroid::jsi to decide whether to compile its JSI registration edge.
|
|
30
|
+
find_package(ReactAndroid REQUIRED CONFIG)
|
|
31
|
+
find_package(fbjni REQUIRED CONFIG)
|
|
32
|
+
|
|
33
|
+
# Autolinked modules (@react-native-quickjs/* declaring reactNativeQuickJSModule,
|
|
34
|
+
# listed by build.gradle) are folded into this .so: whole-archive so each
|
|
35
|
+
# module's QJS_REGISTER_MODULE static initializer runs at load and its exported
|
|
36
|
+
# Java_<module>_nativeAttach survives the link. RNQJS_QUICKJS_FOLDED tells a
|
|
37
|
+
# module's platform JNI to skip JNI_OnLoad (one per .so) and capture the JavaVM
|
|
38
|
+
# from its attach call instead. A module must not ship its own .so -- it would
|
|
39
|
+
# load before the engine and fail to resolve the JS_* API.
|
|
40
|
+
if(DEFINED RNQJS_QUICKJS_MODULES)
|
|
41
|
+
set(REACT_NATIVE_QUICKJS_DIR "${RNQJS_ROOT}" CACHE PATH "" FORCE)
|
|
42
|
+
add_compile_definitions(RNQJS_QUICKJS_FOLDED=1)
|
|
43
|
+
|
|
44
|
+
set(RNQJS_FOLDED_TARGETS "")
|
|
45
|
+
set(RNQJS_REGISTRY_DIRS "")
|
|
46
|
+
foreach(MODULE_DIR IN LISTS RNQJS_QUICKJS_MODULES)
|
|
47
|
+
get_filename_component(MODULE_NAME "${MODULE_DIR}" NAME)
|
|
48
|
+
list(APPEND RNQJS_REGISTRY_DIRS "--dir" "${MODULE_DIR}")
|
|
49
|
+
add_subdirectory("${MODULE_DIR}/android" "quickjs-modules/${MODULE_NAME}")
|
|
50
|
+
if(NOT RNQJS_QUICKJS_MODULE_TARGETS)
|
|
51
|
+
message(FATAL_ERROR
|
|
52
|
+
"${MODULE_DIR}/android did not declare RNQJS_QUICKJS_MODULE_TARGETS "
|
|
53
|
+
"(the static libraries to fold into the engine .so)")
|
|
54
|
+
endif()
|
|
55
|
+
list(APPEND RNQJS_FOLDED_TARGETS ${RNQJS_QUICKJS_MODULE_TARGETS})
|
|
56
|
+
unset(RNQJS_QUICKJS_MODULE_TARGETS)
|
|
57
|
+
endforeach()
|
|
58
|
+
|
|
59
|
+
# The registry TU names each module's install function, making a module that
|
|
60
|
+
# configured without its JSI edge a link error rather than a silently missing
|
|
61
|
+
# global. QuickJSModule.cpp declares registerGeneratedModules() weak; this
|
|
62
|
+
# strong definition in the same .so wins.
|
|
63
|
+
set(RNQJS_REGISTRY "${CMAKE_CURRENT_BINARY_DIR}/generated/QuickJSGeneratedModules.cpp")
|
|
64
|
+
find_program(RNQJS_NODE NAMES node REQUIRED)
|
|
65
|
+
add_custom_command(
|
|
66
|
+
OUTPUT "${RNQJS_REGISTRY}"
|
|
67
|
+
COMMAND "${RNQJS_NODE}"
|
|
68
|
+
"${RNQJS_ROOT}/scripts/generate-module-registry.js"
|
|
69
|
+
${RNQJS_REGISTRY_DIRS}
|
|
70
|
+
--out "${RNQJS_REGISTRY}"
|
|
71
|
+
--quiet
|
|
72
|
+
DEPENDS
|
|
73
|
+
"${RNQJS_ROOT}/scripts/generate-module-registry.js"
|
|
74
|
+
COMMENT "Generating the QuickJS module registry"
|
|
75
|
+
VERBATIM)
|
|
76
|
+
endif()
|
|
77
|
+
|
|
78
|
+
# Every src/ translation unit has to be listed. The Apple build globs src/, so
|
|
79
|
+
# a file added there builds on iOS and fails to link on Android -- an asymmetry
|
|
80
|
+
# worth remembering when adding a source.
|
|
81
|
+
set(RNQJS_QUICKJSINSTANCE_SOURCES
|
|
32
82
|
"${RNQJS_ROOT}/src/bytecode/QuickJSBytecode.cpp"
|
|
33
83
|
"${RNQJS_ROOT}/src/module/QuickJSCompat.cpp"
|
|
34
84
|
"${RNQJS_ROOT}/src/module/QuickJSModule.cpp"
|
|
@@ -38,6 +88,11 @@ add_library(quickjsinstancejni SHARED
|
|
|
38
88
|
src/main/jni/JQuickJSInstance.cpp
|
|
39
89
|
src/main/jni/OnLoad.cpp
|
|
40
90
|
)
|
|
91
|
+
if(DEFINED RNQJS_QUICKJS_MODULES)
|
|
92
|
+
list(APPEND RNQJS_QUICKJSINSTANCE_SOURCES "${RNQJS_REGISTRY}")
|
|
93
|
+
endif()
|
|
94
|
+
|
|
95
|
+
add_library(quickjsinstancejni SHARED ${RNQJS_QUICKJSINSTANCE_SOURCES})
|
|
41
96
|
|
|
42
97
|
target_include_directories(quickjsinstancejni PRIVATE
|
|
43
98
|
"${RNQJS_ROOT}/src/bytecode"
|
|
@@ -46,17 +101,34 @@ target_include_directories(quickjsinstancejni PRIVATE
|
|
|
46
101
|
"${CMAKE_CURRENT_SOURCE_DIR}/src/main/jni"
|
|
47
102
|
)
|
|
48
103
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
104
|
+
# The module archives and the quickjs archive are grouped so the linker
|
|
105
|
+
# resolves references in both directions; the modules are whole-archived because
|
|
106
|
+
# nothing references their platform objects except names Java resolves at
|
|
107
|
+
# runtime, and their initializers must run at load.
|
|
108
|
+
if(RNQJS_FOLDED_TARGETS)
|
|
109
|
+
target_link_libraries(quickjsinstancejni
|
|
110
|
+
-Wl,--start-group
|
|
111
|
+
-Wl,--whole-archive
|
|
112
|
+
${RNQJS_FOLDED_TARGETS}
|
|
113
|
+
-Wl,--no-whole-archive
|
|
114
|
+
quickjs
|
|
115
|
+
-Wl,--end-group
|
|
116
|
+
fbjni::fbjni
|
|
117
|
+
ReactAndroid::jsi
|
|
118
|
+
ReactAndroid::reactnative
|
|
119
|
+
android
|
|
120
|
+
log
|
|
121
|
+
)
|
|
122
|
+
else()
|
|
123
|
+
target_link_libraries(quickjsinstancejni
|
|
124
|
+
quickjs
|
|
125
|
+
fbjni::fbjni
|
|
126
|
+
ReactAndroid::jsi
|
|
127
|
+
ReactAndroid::reactnative
|
|
128
|
+
android
|
|
129
|
+
log
|
|
130
|
+
)
|
|
131
|
+
endif()
|
|
60
132
|
|
|
61
133
|
# --- Chrome DevTools Protocol ----------------------------------------------
|
|
62
134
|
#
|
package/android/build.gradle
CHANGED
|
@@ -2,7 +2,8 @@ buildscript {
|
|
|
2
2
|
ext.ReactNativeQuickJS = [
|
|
3
3
|
kotlinVersion: "2.0.21",
|
|
4
4
|
minSdkVersion: 24,
|
|
5
|
-
compileSdkVersion: 36
|
|
5
|
+
compileSdkVersion: 36,
|
|
6
|
+
ndkVersion: "29.0.14206865"
|
|
6
7
|
]
|
|
7
8
|
|
|
8
9
|
ext.getExtOrDefault = { prop ->
|
|
@@ -31,12 +32,7 @@ apply plugin: "kotlin-android"
|
|
|
31
32
|
android {
|
|
32
33
|
namespace "com.reactnativequickjs.quickjs"
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
// would compile this library with a different toolchain than the app.
|
|
36
|
-
if (rootProject.ext.has("ndkVersion")) {
|
|
37
|
-
ndkVersion rootProject.ext.get("ndkVersion")
|
|
38
|
-
}
|
|
39
|
-
|
|
35
|
+
ndkVersion getExtOrDefault("ndkVersion")
|
|
40
36
|
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
41
37
|
|
|
42
38
|
defaultConfig {
|
|
@@ -59,6 +55,9 @@ android {
|
|
|
59
55
|
arguments "-DANDROID_STL=c++_shared",
|
|
60
56
|
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
|
|
61
57
|
"-DREACT_NATIVE_DIR=${reactNativeDir()}"
|
|
58
|
+
if (quickjsModuleDirs().size() > 0) {
|
|
59
|
+
arguments "-DRNQJS_QUICKJS_MODULES=${quickjsModuleDirs().join(';')}"
|
|
60
|
+
}
|
|
62
61
|
|
|
63
62
|
// Left unset, CMakeLists.txt takes the DevTools backend from the build
|
|
64
63
|
// type. -PrnqjsEnableCdp=true|false forces it either way.
|
|
@@ -142,6 +141,31 @@ def reactNativeArchitectures() {
|
|
|
142
141
|
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
|
143
142
|
}
|
|
144
143
|
|
|
144
|
+
// Autolinked QuickJS modules (@react-native-quickjs/* declaring
|
|
145
|
+
// reactNativeQuickJSModule with an android/ dir), folded into this engine's
|
|
146
|
+
// single .so.
|
|
147
|
+
def quickjsModuleDirs() {
|
|
148
|
+
def engineRoot = projectDir.parentFile
|
|
149
|
+
// Consumer: engineRoot is node_modules/@react-native-quickjs/quickjs.
|
|
150
|
+
// Monorepo: engineRoot is the workspace root.
|
|
151
|
+
def scopes = [engineRoot.parentFile,
|
|
152
|
+
new File(engineRoot, "node_modules/@react-native-quickjs")]
|
|
153
|
+
def dirs = []
|
|
154
|
+
scopes.each { scope ->
|
|
155
|
+
if (scope == null || !scope.exists()) return
|
|
156
|
+
scope.listFiles().each { pkg ->
|
|
157
|
+
def manifest = new File(pkg, "package.json")
|
|
158
|
+
if (!manifest.exists()) return
|
|
159
|
+
def json = new groovy.json.JsonSlurper().parse(manifest)
|
|
160
|
+
if (json.reactNativeQuickJSModule && new File(pkg, "android").exists()
|
|
161
|
+
&& !dirs.contains(pkg.absolutePath)) {
|
|
162
|
+
dirs << pkg.absolutePath
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return dirs
|
|
167
|
+
}
|
|
168
|
+
|
|
145
169
|
dependencies {
|
|
146
170
|
implementation "com.facebook.react:react-android"
|
|
147
171
|
implementation "com.facebook.fbjni:fbjni:0.7.0"
|
|
@@ -26,13 +26,17 @@
|
|
|
26
26
|
"name": "0006-private-symbol-keys.patch",
|
|
27
27
|
"sha256": "cf4ba5df3779f81c1721abab677381de5174ed34323bcb5cee68f2f5cf572edd"
|
|
28
28
|
},
|
|
29
|
+
{
|
|
30
|
+
"name": "0007-stripped-source-tostring.patch",
|
|
31
|
+
"sha256": "5543d3f33108b3666ccc2317b65afc3ae132a8a12e223ed9f6defeaef2367b8b"
|
|
32
|
+
},
|
|
29
33
|
{
|
|
30
34
|
"name": "9999-bc-version-bump.patch",
|
|
31
35
|
"sha256": "1a89bc63c09a7411d3f5d8cacbccbb7c525ad37ec4f7f07ee130f5ade736d428"
|
|
32
36
|
}
|
|
33
37
|
],
|
|
34
38
|
"files": {
|
|
35
|
-
"quickjs.c": "
|
|
39
|
+
"quickjs.c": "9016fa1b5e6682c0c45fef8d1b057aa44cfad95ff1eb50e3432ad520187a86e1",
|
|
36
40
|
"libregexp.c": "27cd5fb4f4f0d8c05fea4cb0ddcd4dede7f39364e87f0f5610cfc25a89908158",
|
|
37
41
|
"libunicode.c": "d88f53190ad772dd09b2beb93ffb8fc3097852f4987485aac8663a00189c43eb",
|
|
38
42
|
"dtoa.c": "a8d2cf2d04db406e0c16acdd95ab0e709d309902865722f41dac07a1ba5676af",
|
|
@@ -43038,17 +43038,23 @@ static JSValue js_function_toString(JSContext *ctx, JSValueConst this_val,
|
|
|
43038
43038
|
int argc, JSValueConst *argv)
|
|
43039
43039
|
{
|
|
43040
43040
|
JSObject *p;
|
|
43041
|
+
bool source_stripped;
|
|
43041
43042
|
JSFunctionKindEnum func_kind = JS_FUNC_NORMAL;
|
|
43042
43043
|
|
|
43043
43044
|
if (check_function(ctx, this_val))
|
|
43044
43045
|
return JS_EXCEPTION;
|
|
43045
43046
|
|
|
43046
43047
|
p = JS_VALUE_GET_OBJ(this_val);
|
|
43048
|
+
source_stripped = false;
|
|
43047
43049
|
if (js_class_has_bytecode(p->class_id)) {
|
|
43048
43050
|
JSFunctionBytecode *b = p->u.func.function_bytecode;
|
|
43049
43051
|
/* `b->source` must be pure ASCII or UTF-8 encoded */
|
|
43050
43052
|
if (b->source)
|
|
43051
43053
|
return JS_NewStringLen(ctx, b->source, b->source_len);
|
|
43054
|
+
/* The embedded source was stripped at compile time (qjsc
|
|
43055
|
+
* --strip-source); only the declaration survives, so say so instead
|
|
43056
|
+
* of pretending this is a native C function. */
|
|
43057
|
+
source_stripped = true;
|
|
43052
43058
|
}
|
|
43053
43059
|
{
|
|
43054
43060
|
JSValue name;
|
|
@@ -43069,7 +43075,10 @@ static JSValue js_function_toString(JSContext *ctx, JSValueConst this_val,
|
|
|
43069
43075
|
pref = "async function *";
|
|
43070
43076
|
break;
|
|
43071
43077
|
}
|
|
43072
|
-
|
|
43078
|
+
if (source_stripped)
|
|
43079
|
+
suff = "() {\n [stripped source]\n}";
|
|
43080
|
+
else
|
|
43081
|
+
suff = "() {\n [native code]\n}";
|
|
43073
43082
|
name = JS_GetProperty(ctx, this_val, JS_ATOM_name);
|
|
43074
43083
|
if (JS_IsUndefined(name))
|
|
43075
43084
|
name = js_empty_string(ctx->rt);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-native-quickjs/quickjs",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.2",
|
|
4
4
|
"description": "QuickJS JSI runtime for React Native, built from source",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"modules/hermes-compat/src",
|
|
36
36
|
"scripts/postinstall.js",
|
|
37
37
|
"scripts/bytecode",
|
|
38
|
+
"scripts/generate-module-registry.js",
|
|
38
39
|
"scripts/react_native_quickjs_pods.rb",
|
|
39
40
|
"*.podspec",
|
|
40
41
|
"bin",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
],
|
|
49
50
|
"workspaces": [
|
|
50
51
|
"modules/text-encoding",
|
|
52
|
+
"modules/intl",
|
|
51
53
|
"example"
|
|
52
54
|
],
|
|
53
55
|
"scripts": {
|
|
@@ -66,8 +68,9 @@
|
|
|
66
68
|
"example": "npm run --workspace @react-native-quickjs/example",
|
|
67
69
|
"docs:dev": "vitepress dev docs",
|
|
68
70
|
"docs:build": "vitepress build docs",
|
|
69
|
-
"release
|
|
70
|
-
"release:alpha": "
|
|
71
|
+
"release": "node scripts/release.js",
|
|
72
|
+
"release:alpha": "node scripts/release.js --tag alpha",
|
|
73
|
+
"release:module": "node scripts/release-module.js"
|
|
71
74
|
},
|
|
72
75
|
"peerDependencies": {
|
|
73
76
|
"react": "*",
|
|
@@ -99,6 +102,6 @@
|
|
|
99
102
|
}
|
|
100
103
|
},
|
|
101
104
|
"dependencies": {
|
|
102
|
-
"@react-native-quickjs/text-encoding": "1.0.
|
|
105
|
+
"@react-native-quickjs/text-encoding": "1.0.1"
|
|
103
106
|
}
|
|
104
107
|
}
|
|
@@ -0,0 +1,234 @@
|
|
|
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
|
+
|
|
9
|
+
/*
|
|
10
|
+
* Emits a C++ file that calls each autolinked QuickJS module's install
|
|
11
|
+
* function by name.
|
|
12
|
+
*
|
|
13
|
+
* Naming the function is what stops the linker dropping a static library's
|
|
14
|
+
* object file, which would otherwise leave the module silently uninstalled.
|
|
15
|
+
*
|
|
16
|
+
* node scripts/generate-module-registry.js [--dir <pkg> ...] [--out <file>]
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
|
|
22
|
+
const argv = process.argv.slice(2);
|
|
23
|
+
const opt = (name, dflt = null) => {
|
|
24
|
+
const i = argv.indexOf(`--${name}`);
|
|
25
|
+
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
29
|
+
console.log(`
|
|
30
|
+
generate-module-registry — autolink QuickJS modules
|
|
31
|
+
|
|
32
|
+
node scripts/generate-module-registry.js [options]
|
|
33
|
+
|
|
34
|
+
Options
|
|
35
|
+
--root <dir> project root to scan from (default: cwd)
|
|
36
|
+
--out <file> output path (default: <root>/build/QuickJSGeneratedModules.cpp)
|
|
37
|
+
--dir <dir> an explicit module package directory (repeatable). When given,
|
|
38
|
+
the registry is exactly these modules -- authoritative, for an
|
|
39
|
+
app build that must only reference linked modules.
|
|
40
|
+
--only <names> keep only these discovered modules (comma-separated names)
|
|
41
|
+
--json print the discovered modules as JSON and write nothing
|
|
42
|
+
--quiet suppress the summary`);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const root = path.resolve(opt('root', process.cwd()));
|
|
47
|
+
const outPath = path.resolve(
|
|
48
|
+
opt('out', path.join(root, 'build', 'QuickJSGeneratedModules.cpp'))
|
|
49
|
+
);
|
|
50
|
+
const only = opt('only', null) ? new Set(opt('only').split(',')) : null;
|
|
51
|
+
const asJson = argv.includes('--json');
|
|
52
|
+
const quiet = argv.includes('--quiet');
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Directories to scan. Monorepos hoist to the root `node_modules`, and this
|
|
56
|
+
* repository keeps its first-party modules in `modules/`, uninstalled.
|
|
57
|
+
*/
|
|
58
|
+
function candidateDirs() {
|
|
59
|
+
const dirs = [];
|
|
60
|
+
const nodeModules = path.join(root, 'node_modules');
|
|
61
|
+
if (fs.existsSync(nodeModules)) dirs.push(nodeModules);
|
|
62
|
+
|
|
63
|
+
const modules = path.join(root, 'modules');
|
|
64
|
+
if (fs.existsSync(modules)) dirs.push(modules);
|
|
65
|
+
|
|
66
|
+
return dirs;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Reads a package.json, or null when it is missing or invalid. */
|
|
70
|
+
function readPackage(dir) {
|
|
71
|
+
try {
|
|
72
|
+
const raw = fs.readFileSync(path.join(dir, 'package.json'), 'utf8');
|
|
73
|
+
return JSON.parse(raw);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function moduleFromDir(dir) {
|
|
80
|
+
const pkg = readPackage(dir);
|
|
81
|
+
const spec = pkg && pkg.reactNativeQuickJSModule;
|
|
82
|
+
if (!spec || !spec.install) return null;
|
|
83
|
+
return {
|
|
84
|
+
name: pkg.name || path.basename(dir),
|
|
85
|
+
install: spec.install,
|
|
86
|
+
header: spec.header || null,
|
|
87
|
+
priority: typeof spec.priority === 'number' ? spec.priority : 0,
|
|
88
|
+
dir,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function discover() {
|
|
93
|
+
const found = new Map(); // name -> { name, install, header, dir }
|
|
94
|
+
|
|
95
|
+
for (const base of candidateDirs()) {
|
|
96
|
+
let entries;
|
|
97
|
+
try {
|
|
98
|
+
entries = fs.readdirSync(base, { withFileTypes: true });
|
|
99
|
+
} catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
105
|
+
|
|
106
|
+
// Scoped packages nest one level deeper: node_modules/@scope/name.
|
|
107
|
+
const dirs = entry.name.startsWith('@')
|
|
108
|
+
? fs
|
|
109
|
+
.readdirSync(path.join(base, entry.name), { withFileTypes: true })
|
|
110
|
+
.filter((e) => e.isDirectory() || e.isSymbolicLink())
|
|
111
|
+
.map((e) => path.join(base, entry.name, e.name))
|
|
112
|
+
: [path.join(base, entry.name)];
|
|
113
|
+
|
|
114
|
+
for (const dir of dirs) {
|
|
115
|
+
const m = moduleFromDir(dir);
|
|
116
|
+
if (m && !found.has(m.name)) found.set(m.name, m); // first wins
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Deterministic output, so an unchanged build does not retrigger everything
|
|
122
|
+
// downstream of the registry.
|
|
123
|
+
return [...found.values()].sort(
|
|
124
|
+
(a, b) => a.priority - b.priority || a.name.localeCompare(b.name)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --dir is authoritative: the registry becomes exactly these modules, which is
|
|
129
|
+
// what an app build wants. node_modules holds packages that are present but not
|
|
130
|
+
// linked (transitive dependencies), and referencing their symbols would fail
|
|
131
|
+
// the link.
|
|
132
|
+
const explicitDirs = [];
|
|
133
|
+
for (let i = 0; i < argv.length; i++) {
|
|
134
|
+
if (argv[i] === '--dir' && argv[i + 1]) explicitDirs.push(path.resolve(argv[i + 1]));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let modules;
|
|
138
|
+
if (explicitDirs.length > 0) {
|
|
139
|
+
const seen = new Map();
|
|
140
|
+
for (const dir of explicitDirs) {
|
|
141
|
+
const m = moduleFromDir(dir);
|
|
142
|
+
if (m && !seen.has(m.name)) seen.set(m.name, m);
|
|
143
|
+
}
|
|
144
|
+
modules = [...seen.values()].sort(
|
|
145
|
+
(a, b) => a.priority - b.priority || a.name.localeCompare(b.name)
|
|
146
|
+
);
|
|
147
|
+
} else {
|
|
148
|
+
modules = discover();
|
|
149
|
+
if (only) {
|
|
150
|
+
// An allow-list over the discovered modules.
|
|
151
|
+
modules = modules.filter((m) => only.has(m.name));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (asJson) {
|
|
156
|
+
console.log(JSON.stringify(modules, null, 2));
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const isValidIdentifier = (s) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s);
|
|
161
|
+
|
|
162
|
+
for (const m of modules) {
|
|
163
|
+
if (!isValidIdentifier(m.install)) {
|
|
164
|
+
console.error(
|
|
165
|
+
`[generate-module-registry] ${m.name}: reactNativeQuickJSModule.install ` +
|
|
166
|
+
`must be a C identifier, got ${JSON.stringify(m.install)}`
|
|
167
|
+
);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const lines = [];
|
|
173
|
+
lines.push('// Generated by scripts/generate-module-registry.js — do not edit.');
|
|
174
|
+
lines.push('');
|
|
175
|
+
lines.push('#include <jsi/jsi.h>');
|
|
176
|
+
lines.push('');
|
|
177
|
+
lines.push('#include "QuickJSModule.h"');
|
|
178
|
+
lines.push('');
|
|
179
|
+
|
|
180
|
+
if (modules.length === 0) {
|
|
181
|
+
lines.push('// No autolinked modules were found.');
|
|
182
|
+
lines.push('');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
lines.push('extern "C" {');
|
|
186
|
+
for (const m of modules) {
|
|
187
|
+
lines.push(`// ${m.name}`);
|
|
188
|
+
lines.push(`void ${m.install}(facebook::jsi::Runtime &);`);
|
|
189
|
+
}
|
|
190
|
+
lines.push('}');
|
|
191
|
+
lines.push('');
|
|
192
|
+
lines.push('namespace qjs {');
|
|
193
|
+
lines.push('');
|
|
194
|
+
lines.push('void registerGeneratedModules() {');
|
|
195
|
+
if (modules.length === 0) {
|
|
196
|
+
lines.push(' // Nothing to register.');
|
|
197
|
+
}
|
|
198
|
+
for (const m of modules) {
|
|
199
|
+
lines.push(` registerModule("${m.name}", ${m.install}, ${m.priority});`);
|
|
200
|
+
}
|
|
201
|
+
lines.push('}');
|
|
202
|
+
lines.push('');
|
|
203
|
+
lines.push('} // namespace qjs');
|
|
204
|
+
lines.push('');
|
|
205
|
+
|
|
206
|
+
const output = lines.join('\n');
|
|
207
|
+
|
|
208
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
209
|
+
|
|
210
|
+
// Only write when the content differs, so an unchanged registry does not
|
|
211
|
+
// retrigger a rebuild of everything downstream of it.
|
|
212
|
+
let existing = null;
|
|
213
|
+
try {
|
|
214
|
+
existing = fs.readFileSync(outPath, 'utf8');
|
|
215
|
+
} catch {
|
|
216
|
+
/* first run */
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (existing !== output) {
|
|
220
|
+
fs.writeFileSync(outPath, output);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!quiet) {
|
|
224
|
+
if (modules.length === 0) {
|
|
225
|
+
console.log('[generate-module-registry] no QuickJS modules found');
|
|
226
|
+
} else {
|
|
227
|
+
console.log(
|
|
228
|
+
`[generate-module-registry] ${modules.length} module(s) -> ${path.relative(root, outPath)}`
|
|
229
|
+
);
|
|
230
|
+
for (const m of modules) {
|
|
231
|
+
console.log(` ${m.name} -> ${m.install}()`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
# end
|
|
18
18
|
# end
|
|
19
19
|
|
|
20
|
+
require "json"
|
|
21
|
+
require "pathname"
|
|
22
|
+
|
|
20
23
|
# react-native-worklets and react-native-reanimated declare
|
|
21
24
|
# `s.dependency 'React-hermes'` unconditionally, and use_quickjs! never declares
|
|
22
25
|
# that pod, so `pod install` cannot resolve it. Neither uses anything from it:
|
|
@@ -134,6 +137,7 @@ end
|
|
|
134
137
|
|
|
135
138
|
def react_native_quickjs_post_install(installer)
|
|
136
139
|
react_native_quickjs_add_bytecode_phase(installer)
|
|
140
|
+
react_native_quickjs_add_module_registry(installer)
|
|
137
141
|
|
|
138
142
|
# Debug only, matching the gate React Native puts on its own inspector.
|
|
139
143
|
# QuickJSInstance::debuggerEnabledByDefault() already refuses to attach in a
|
|
@@ -235,3 +239,129 @@ def react_native_quickjs_append(installer, pod_name, setting, value, debug_only:
|
|
|
235
239
|
config.build_settings[setting] = "#{current} #{value}"
|
|
236
240
|
end
|
|
237
241
|
end
|
|
242
|
+
|
|
243
|
+
# The generated module registry, compiled into the app target.
|
|
244
|
+
#
|
|
245
|
+
# A QuickJS module's QJS_REGISTER_MODULE static initializer is dropped by the
|
|
246
|
+
# linker from a static archive unless something references its object file, so
|
|
247
|
+
# the registry calls each module's install function by name. The reference must
|
|
248
|
+
# live in the last-linked thing -- the app target -- because CocoaPods orders
|
|
249
|
+
# module pods before the engine pod. A script phase regenerates the TU below
|
|
250
|
+
# before Sources compile; the host build exercises the same path in
|
|
251
|
+
# quickjs_generated_modules_tests.
|
|
252
|
+
MODULE_REGISTRY_PHASE = "[ReactNativeQuickJS] Generate module registry"
|
|
253
|
+
|
|
254
|
+
def react_native_quickjs_add_module_registry(installer)
|
|
255
|
+
engine_root = File.expand_path("..", __dir__)
|
|
256
|
+
generator = "scripts/generate-module-registry.js"
|
|
257
|
+
|
|
258
|
+
sandbox = Pod::Config.instance.sandbox
|
|
259
|
+
sandbox_root = sandbox.root.to_s
|
|
260
|
+
|
|
261
|
+
module_dirs = []
|
|
262
|
+
dev_pods = sandbox.send(:development_pods)
|
|
263
|
+
(installer.pod_targets || []).each do |pod_target|
|
|
264
|
+
next unless pod_target.name.start_with?("react-native-quickjs-")
|
|
265
|
+
|
|
266
|
+
# The podspec's directory is the package root; for a local (file:) pod it
|
|
267
|
+
# comes from CocoaPods' development_pods storage, not a layout guess.
|
|
268
|
+
spec_file = dev_pods[pod_target.pod_name].to_s
|
|
269
|
+
pkg_root = spec_file.empty? ? "" : File.dirname(spec_file)
|
|
270
|
+
if pkg_root.empty? || !File.exist?(File.join(pkg_root, "package.json"))
|
|
271
|
+
spec_file = pod_target.root_spec.defined_in_file
|
|
272
|
+
pkg_root = spec_file ? File.dirname(spec_file) : ""
|
|
273
|
+
end
|
|
274
|
+
next if pkg_root.empty?
|
|
275
|
+
pkg = File.join(pkg_root, "package.json")
|
|
276
|
+
next unless File.exist?(pkg)
|
|
277
|
+
next unless JSON.parse(File.read(pkg))["reactNativeQuickJSModule"]
|
|
278
|
+
module_dirs << pkg_root
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Warn rather than silently produce an empty registry when modules are linked.
|
|
282
|
+
if module_dirs.empty? && (installer.pods_project)
|
|
283
|
+
(installer.pods_project.targets).each do |t|
|
|
284
|
+
next unless t.name.start_with?("react-native-quickjs-")
|
|
285
|
+
Pod::UI.warn(
|
|
286
|
+
"[ReactNativeQuickJS] found pod #{t.name} but no package.json with " \
|
|
287
|
+
"reactNativeQuickJSModule next to it; module registry will be empty."
|
|
288
|
+
)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
return if module_dirs.empty?
|
|
293
|
+
|
|
294
|
+
node_binary = ENV["NODE_BINARY"] || "node"
|
|
295
|
+
|
|
296
|
+
# The registry TU includes QuickJSModule.h (which includes jsi/jsi.h). The
|
|
297
|
+
# module pods expose those headers to themselves, not to the app, so the app
|
|
298
|
+
# target gets the engine's module headers and the Pods React-jsi headers.
|
|
299
|
+
jsi_headers = File.join(sandbox_root, "Headers", "Public", "React-jsi")
|
|
300
|
+
engine_src = File.join(engine_root, "src", "module")
|
|
301
|
+
engine_headers = [
|
|
302
|
+
"\"#{jsi_headers}\"",
|
|
303
|
+
"\"#{engine_src}\"",
|
|
304
|
+
].select { |p| p.include?("$(PODS_ROOT)") || File.directory?(p.tr("\"", "")) }
|
|
305
|
+
|
|
306
|
+
installer.aggregate_targets.map(&:user_project).uniq(&:path).compact.each do |project|
|
|
307
|
+
project.native_targets.each do |target|
|
|
308
|
+
next unless target.product_type == "com.apple.product-type.application"
|
|
309
|
+
|
|
310
|
+
project_dir = File.dirname(project.path)
|
|
311
|
+
# The generated TU lives under the app project and is addressed with
|
|
312
|
+
# $SRCROOT, so nothing machine-specific is written into the project.
|
|
313
|
+
out = File.join(project_dir, "build", "generated", "QuickJSGeneratedModules.cpp")
|
|
314
|
+
|
|
315
|
+
target.build_configurations.each do |config|
|
|
316
|
+
paths = [config.build_settings["HEADER_SEARCH_PATHS"] || "$(inherited)"]
|
|
317
|
+
paths = [paths] unless paths.is_a?(Array)
|
|
318
|
+
paths = paths.flatten
|
|
319
|
+
paths.concat(engine_headers)
|
|
320
|
+
config.build_settings["HEADER_SEARCH_PATHS"] = paths.uniq.join(" ")
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Reuse an existing phase (a re-run refreshes it rather than accumulating
|
|
324
|
+
# another) and keep it ordered before Sources.
|
|
325
|
+
phase = target.build_phases.find do |p|
|
|
326
|
+
p.respond_to?(:display_name) && p.display_name == MODULE_REGISTRY_PHASE
|
|
327
|
+
end
|
|
328
|
+
phase ||= target.new_shell_script_build_phase(MODULE_REGISTRY_PHASE)
|
|
329
|
+
|
|
330
|
+
# The generated file is declared as an output so Compile Sources can
|
|
331
|
+
# consume it ("Build input file cannot be found" otherwise). Engine and
|
|
332
|
+
# module paths are resolved at build time through node.
|
|
333
|
+
phase.output_paths = ["$(SRCROOT)/build/generated/QuickJSGeneratedModules.cpp"]
|
|
334
|
+
rel_dirs = module_dirs.map do |d|
|
|
335
|
+
Pathname.new(d).relative_path_from(Pathname.new(engine_root)).to_s
|
|
336
|
+
end
|
|
337
|
+
phase.shell_script = <<~SH
|
|
338
|
+
set -e
|
|
339
|
+
[ -f "$SRCROOT/.xcode.env" ] && . "$SRCROOT/.xcode.env"
|
|
340
|
+
[ -f "$SRCROOT/.xcode.env.local" ] && . "$SRCROOT/.xcode.env.local"
|
|
341
|
+
: "${NODE_BINARY:=#{node_binary}}"
|
|
342
|
+
ENGINE="$("$NODE_BINARY" --print "require.resolve('@react-native-quickjs/quickjs/package.json', {paths: ['$SRCROOT']})" | xargs dirname)"
|
|
343
|
+
OUT="$SRCROOT/build/generated/QuickJSGeneratedModules.cpp"
|
|
344
|
+
mkdir -p "$(dirname "$OUT")"
|
|
345
|
+
"$NODE_BINARY" "$ENGINE/#{generator}" --dir "$ENGINE/#{rel_dirs.join('" --dir "$ENGINE/')}" \\
|
|
346
|
+
--out "$OUT" --quiet
|
|
347
|
+
SH
|
|
348
|
+
|
|
349
|
+
compile = target.build_phases.find do |p|
|
|
350
|
+
p.respond_to?(:display_name) && p.display_name == "Sources"
|
|
351
|
+
end
|
|
352
|
+
target.build_phases.delete(phase)
|
|
353
|
+
index = compile ? target.build_phases.index(compile) : nil
|
|
354
|
+
target.build_phases.insert(index, phase) if index
|
|
355
|
+
|
|
356
|
+
rel = Pathname.new(out).relative_path_from(Pathname.new(project_dir)).to_s
|
|
357
|
+
ref = project.main_group.files.find { |f| f.path == rel }
|
|
358
|
+
ref ||= project.main_group.new_file(rel)
|
|
359
|
+
target.source_build_phase.add_file_reference(ref)
|
|
360
|
+
|
|
361
|
+
Pod::UI.puts(
|
|
362
|
+
"[ReactNativeQuickJS] module registry -> #{module_dirs.map { |d| File.basename(d) }.join(', ')}".green
|
|
363
|
+
)
|
|
364
|
+
end
|
|
365
|
+
project.save
|
|
366
|
+
end
|
|
367
|
+
end
|
|
@@ -27,6 +27,14 @@ std::vector<ModuleRegistration> ®istry() {
|
|
|
27
27
|
|
|
28
28
|
} // namespace
|
|
29
29
|
|
|
30
|
+
// Weak default: linking quickjs_jsi without the generated registry TU still
|
|
31
|
+
// links, and a target that compiles that TU overrides this (clang/gcc).
|
|
32
|
+
#if defined(__GNUC__) || defined(__clang__)
|
|
33
|
+
__attribute__((weak)) void registerGeneratedModules() {}
|
|
34
|
+
#else
|
|
35
|
+
void registerGeneratedModules() {}
|
|
36
|
+
#endif
|
|
37
|
+
|
|
30
38
|
void registerModule(const ModuleRegistration &module) {
|
|
31
39
|
if (module.install == nullptr || module.name == nullptr) {
|
|
32
40
|
return;
|
|
@@ -54,6 +62,7 @@ const std::vector<ModuleRegistration> ®isteredModules() {
|
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
void installModules(jsi::Runtime &runtime) {
|
|
65
|
+
registerGeneratedModules();
|
|
57
66
|
for (const auto &module : registry()) {
|
|
58
67
|
module.install(runtime);
|
|
59
68
|
}
|
|
@@ -62,6 +62,16 @@ inline void registerModule(
|
|
|
62
62
|
registerModule(ModuleRegistration{name, install, priority});
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Installs autolinked modules. Defined in a translation unit generated by
|
|
67
|
+
* scripts/generate-module-registry.js, which names each module's install
|
|
68
|
+
* function so the linker cannot drop its static-library object file (the
|
|
69
|
+
* failure plain QJS_REGISTER_MODULE is subject to). Called from
|
|
70
|
+
* installModules(); registration is idempotent, so a module reached by both
|
|
71
|
+
* routes installs once.
|
|
72
|
+
*/
|
|
73
|
+
void registerGeneratedModules();
|
|
74
|
+
|
|
65
75
|
/// `makeQuickJSRuntime()` calls this. Call it yourself only for a runtime you
|
|
66
76
|
/// built by other means.
|
|
67
77
|
void installModules(jsi::Runtime &runtime);
|