Skip to content

Commit 5b41f8e

Browse files
committed
feat: esm http loading for hmr enhancements
1 parent 116a45d commit 5b41f8e

File tree

6 files changed

+459
-55
lines changed

6 files changed

+459
-55
lines changed

test-app/runtime/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ add_library(
147147
src/main/cpp/URLImpl.cpp
148148
src/main/cpp/URLSearchParamsImpl.cpp
149149
src/main/cpp/URLPatternImpl.cpp
150+
src/main/cpp/HMRSupport.cpp
151+
src/main/cpp/DevFlags.cpp
150152

151153
# V8 inspector source files will be included only in Release mode
152154
${INSPECTOR_SOURCES}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// DevFlags.cpp
2+
#include "DevFlags.h"
3+
#include "JEnv.h"
4+
#include <atomic>
5+
#include <mutex>
6+
7+
namespace tns {
8+
9+
bool IsScriptLoadingLogEnabled() {
10+
static std::atomic<int> cached{-1}; // -1 unknown, 0 false, 1 true
11+
int v = cached.load(std::memory_order_acquire);
12+
if (v != -1) {
13+
return v == 1;
14+
}
15+
16+
static std::once_flag initFlag;
17+
std::call_once(initFlag, []() {
18+
bool enabled = false;
19+
try {
20+
JEnv env;
21+
jclass runtimeClass = env.FindClass("com/tns/Runtime");
22+
if (runtimeClass != nullptr) {
23+
jmethodID mid = env.GetStaticMethodID(runtimeClass, "getLogScriptLoadingEnabled", "()Z");
24+
if (mid != nullptr) {
25+
jboolean res = env.CallStaticBooleanMethod(runtimeClass, mid);
26+
enabled = (res == JNI_TRUE);
27+
}
28+
}
29+
} catch (...) {
30+
// keep default false
31+
}
32+
cached.store(enabled ? 1 : 0, std::memory_order_release);
33+
});
34+
35+
return cached.load(std::memory_order_acquire) == 1;
36+
}
37+
38+
} // namespace tns
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// DevFlags.h
2+
#pragma once
3+
4+
namespace tns {
5+
6+
// Fast cached flag: whether to log script loading diagnostics.
7+
// First call queries Java once; subsequent calls are atomic loads only.
8+
bool IsScriptLoadingLogEnabled();
9+
10+
}
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
// HMRSupport.cpp
2+
#include "HMRSupport.h"
3+
#include "ArgConverter.h"
4+
#include "JEnv.h"
5+
#include <algorithm>
6+
#include <jni.h>
7+
#include <unordered_map>
8+
#include <cstring>
9+
10+
namespace tns {
11+
12+
static inline bool StartsWith(const std::string& s, const char* prefix) {
13+
size_t n = strlen(prefix);
14+
return s.size() >= n && s.compare(0, n, prefix) == 0;
15+
}
16+
17+
// Per-module hot data and callbacks. Keyed by canonical module path (file path or URL).
18+
static std::unordered_map<std::string, v8::Global<v8::Object>> g_hotData;
19+
static std::unordered_map<std::string, std::vector<v8::Global<v8::Function>>> g_hotAccept;
20+
static std::unordered_map<std::string, std::vector<v8::Global<v8::Function>>> g_hotDispose;
21+
22+
v8::Local<v8::Object> GetOrCreateHotData(v8::Isolate* isolate, const std::string& key) {
23+
auto it = g_hotData.find(key);
24+
if (it != g_hotData.end() && !it->second.IsEmpty()) {
25+
return it->second.Get(isolate);
26+
}
27+
v8::Local<v8::Object> obj = v8::Object::New(isolate);
28+
g_hotData[key].Reset(isolate, obj);
29+
return obj;
30+
}
31+
32+
void RegisterHotAccept(v8::Isolate* isolate, const std::string& key, v8::Local<v8::Function> cb) {
33+
if (cb.IsEmpty()) return;
34+
g_hotAccept[key].emplace_back(v8::Global<v8::Function>(isolate, cb));
35+
}
36+
37+
void RegisterHotDispose(v8::Isolate* isolate, const std::string& key, v8::Local<v8::Function> cb) {
38+
if (cb.IsEmpty()) return;
39+
g_hotDispose[key].emplace_back(v8::Global<v8::Function>(isolate, cb));
40+
}
41+
42+
std::vector<v8::Local<v8::Function>> GetHotAcceptCallbacks(v8::Isolate* isolate, const std::string& key) {
43+
std::vector<v8::Local<v8::Function>> out;
44+
auto it = g_hotAccept.find(key);
45+
if (it != g_hotAccept.end()) {
46+
for (auto& gfn : it->second) {
47+
if (!gfn.IsEmpty()) out.push_back(gfn.Get(isolate));
48+
}
49+
}
50+
return out;
51+
}
52+
53+
std::vector<v8::Local<v8::Function>> GetHotDisposeCallbacks(v8::Isolate* isolate, const std::string& key) {
54+
std::vector<v8::Local<v8::Function>> out;
55+
auto it = g_hotDispose.find(key);
56+
if (it != g_hotDispose.end()) {
57+
for (auto& gfn : it->second) {
58+
if (!gfn.IsEmpty()) out.push_back(gfn.Get(isolate));
59+
}
60+
}
61+
return out;
62+
}
63+
64+
void InitializeImportMetaHot(v8::Isolate* isolate,
65+
v8::Local<v8::Context> context,
66+
v8::Local<v8::Object> importMeta,
67+
const std::string& modulePath) {
68+
using v8::Function;
69+
using v8::FunctionCallbackInfo;
70+
using v8::Local;
71+
using v8::Object;
72+
using v8::String;
73+
using v8::Value;
74+
75+
v8::HandleScope scope(isolate);
76+
77+
auto makeKeyData = [&](const std::string& key) -> Local<Value> {
78+
return ArgConverter::ConvertToV8String(isolate, key);
79+
};
80+
81+
auto acceptCb = [](const FunctionCallbackInfo<Value>& info) {
82+
v8::Isolate* iso = info.GetIsolate();
83+
Local<Value> data = info.Data();
84+
std::string key;
85+
if (!data.IsEmpty()) {
86+
v8::String::Utf8Value s(iso, data);
87+
key = *s ? *s : "";
88+
}
89+
v8::Local<v8::Function> cb;
90+
if (info.Length() >= 1 && info[0]->IsFunction()) {
91+
cb = info[0].As<v8::Function>();
92+
} else if (info.Length() >= 2 && info[1]->IsFunction()) {
93+
cb = info[1].As<v8::Function>();
94+
}
95+
if (!cb.IsEmpty()) {
96+
RegisterHotAccept(iso, key, cb);
97+
}
98+
info.GetReturnValue().Set(v8::Undefined(iso));
99+
};
100+
101+
auto disposeCb = [](const FunctionCallbackInfo<Value>& info) {
102+
v8::Isolate* iso = info.GetIsolate();
103+
Local<Value> data = info.Data();
104+
std::string key;
105+
if (!data.IsEmpty()) { v8::String::Utf8Value s(iso, data); key = *s ? *s : ""; }
106+
if (info.Length() >= 1 && info[0]->IsFunction()) {
107+
RegisterHotDispose(iso, key, info[0].As<v8::Function>());
108+
}
109+
info.GetReturnValue().Set(v8::Undefined(iso));
110+
};
111+
112+
auto declineCb = [](const FunctionCallbackInfo<Value>& info) {
113+
info.GetReturnValue().Set(v8::Undefined(info.GetIsolate()));
114+
};
115+
116+
auto invalidateCb = [](const FunctionCallbackInfo<Value>& info) {
117+
info.GetReturnValue().Set(v8::Undefined(info.GetIsolate()));
118+
};
119+
120+
Local<Object> hot = Object::New(isolate);
121+
hot->CreateDataProperty(context, ArgConverter::ConvertToV8String(isolate, "data"),
122+
GetOrCreateHotData(isolate, modulePath)).Check();
123+
hot->CreateDataProperty(context, ArgConverter::ConvertToV8String(isolate, "prune"),
124+
v8::Boolean::New(isolate, false)).Check();
125+
hot->CreateDataProperty(
126+
context, ArgConverter::ConvertToV8String(isolate, "accept"),
127+
v8::Function::New(context, acceptCb, makeKeyData(modulePath)).ToLocalChecked()).Check();
128+
hot->CreateDataProperty(
129+
context, ArgConverter::ConvertToV8String(isolate, "dispose"),
130+
v8::Function::New(context, disposeCb, makeKeyData(modulePath)).ToLocalChecked()).Check();
131+
hot->CreateDataProperty(
132+
context, ArgConverter::ConvertToV8String(isolate, "decline"),
133+
v8::Function::New(context, declineCb, makeKeyData(modulePath)).ToLocalChecked()).Check();
134+
hot->CreateDataProperty(
135+
context, ArgConverter::ConvertToV8String(isolate, "invalidate"),
136+
v8::Function::New(context, invalidateCb, makeKeyData(modulePath)).ToLocalChecked()).Check();
137+
138+
importMeta->CreateDataProperty(context, ArgConverter::ConvertToV8String(isolate, "hot"), hot).Check();
139+
}
140+
141+
// Drop fragments and normalize parameters for consistent registry keys.
142+
std::string CanonicalizeHttpUrlKey(const std::string& url) {
143+
if (!(StartsWith(url, "http://") || StartsWith(url, "https://"))) {
144+
return url;
145+
}
146+
// Remove fragment
147+
size_t hashPos = url.find('#');
148+
std::string noHash = (hashPos == std::string::npos) ? url : url.substr(0, hashPos);
149+
150+
// Strip ?import markers and sort remaining query params for stability
151+
size_t qPos = noHash.find('?');
152+
if (qPos == std::string::npos) return noHash;
153+
std::string originAndPath = noHash.substr(0, qPos);
154+
std::string query = noHash.substr(qPos + 1);
155+
std::vector<std::string> kept;
156+
size_t start = 0;
157+
while (start <= query.size()) {
158+
size_t amp = query.find('&', start);
159+
std::string pair = (amp == std::string::npos) ? query.substr(start) : query.substr(start, amp - start);
160+
if (!pair.empty()) {
161+
size_t eq = pair.find('=');
162+
std::string name = (eq == std::string::npos) ? pair : pair.substr(0, eq);
163+
if (!(name == "import")) kept.push_back(pair);
164+
}
165+
if (amp == std::string::npos) break;
166+
start = amp + 1;
167+
}
168+
if (kept.empty()) return originAndPath;
169+
std::sort(kept.begin(), kept.end());
170+
std::string rebuilt = originAndPath + "?";
171+
for (size_t i = 0; i < kept.size(); i++) {
172+
if (i > 0) rebuilt += "&";
173+
rebuilt += kept[i];
174+
}
175+
return rebuilt;
176+
}
177+
178+
// Minimal HTTP fetch using java.net.* via JNI. Returns true on success (2xx) and non-empty body.
179+
bool HttpFetchText(const std::string& url, std::string& out, std::string& contentType, int& status) {
180+
out.clear();
181+
contentType.clear();
182+
status = 0;
183+
try {
184+
JEnv env;
185+
186+
jclass clsURL = env.FindClass("java/net/URL");
187+
if (!clsURL) return false;
188+
jmethodID urlCtor = env.GetMethodID(clsURL, "<init>", "(Ljava/lang/String;)V");
189+
jmethodID openConnection = env.GetMethodID(clsURL, "openConnection", "()Ljava/net/URLConnection;");
190+
jstring jUrlStr = env.NewStringUTF(url.c_str());
191+
jobject urlObj = env.NewObject(clsURL, urlCtor, jUrlStr);
192+
193+
jobject conn = env.CallObjectMethod(urlObj, openConnection);
194+
if (!conn) return false;
195+
196+
jclass clsConn = env.GetObjectClass(conn);
197+
jmethodID setConnectTimeout = env.GetMethodID(clsConn, "setConnectTimeout", "(I)V");
198+
jmethodID setReadTimeout = env.GetMethodID(clsConn, "setReadTimeout", "(I)V");
199+
jmethodID setReqProp = env.GetMethodID(clsConn, "setRequestProperty", "(Ljava/lang/String;Ljava/lang/String;)V");
200+
env.CallVoidMethod(conn, setConnectTimeout, 5000);
201+
env.CallVoidMethod(conn, setReadTimeout, 5000);
202+
env.CallVoidMethod(conn, setReqProp, env.NewStringUTF("Accept"), env.NewStringUTF("application/javascript, text/javascript, */*;q=0.1"));
203+
env.CallVoidMethod(conn, setReqProp, env.NewStringUTF("Accept-Encoding"), env.NewStringUTF("identity"));
204+
205+
// Try to get status via HttpURLConnection if possible
206+
jclass clsHttp = env.FindClass("java/net/HttpURLConnection");
207+
bool isHttp = clsHttp && env.IsInstanceOf(conn, clsHttp);
208+
jmethodID getResponseCode = isHttp ? env.GetMethodID(clsHttp, "getResponseCode", "()I") : nullptr;
209+
if (isHttp && getResponseCode) {
210+
status = env.CallIntMethod(conn, getResponseCode);
211+
}
212+
213+
// Read InputStream
214+
jmethodID getInputStream = env.GetMethodID(clsConn, "getInputStream", "()Ljava/io/InputStream;");
215+
jobject inStream = env.CallObjectMethod(conn, getInputStream);
216+
if (!inStream) return false;
217+
218+
jclass clsIS = env.GetObjectClass(inStream);
219+
jmethodID readMethod = env.GetMethodID(clsIS, "read", "([B)I");
220+
jmethodID closeIS = env.GetMethodID(clsIS, "close", "()V");
221+
222+
jclass clsBAOS = env.FindClass("java/io/ByteArrayOutputStream");
223+
jmethodID baosCtor = env.GetMethodID(clsBAOS, "<init>", "()V");
224+
jmethodID baosWrite = env.GetMethodID(clsBAOS, "write", "([BII)V");
225+
jmethodID baosToByteArray = env.GetMethodID(clsBAOS, "toByteArray", "()[B");
226+
jmethodID baosClose = env.GetMethodID(clsBAOS, "close", "()V");
227+
jobject baos = env.NewObject(clsBAOS, baosCtor);
228+
229+
jbyteArray buffer = env.NewByteArray(8192);
230+
while (true) {
231+
jint n = env.CallIntMethod(inStream, readMethod, buffer);
232+
if (n <= 0) break;
233+
env.CallVoidMethod(baos, baosWrite, buffer, 0, n);
234+
}
235+
236+
env.CallVoidMethod(inStream, closeIS);
237+
jbyteArray bytes = (jbyteArray) env.CallObjectMethod(baos, baosToByteArray);
238+
env.CallVoidMethod(baos, baosClose);
239+
240+
if (!bytes) return false;
241+
jsize len = env.GetArrayLength(bytes);
242+
out.resize(static_cast<size_t>(len));
243+
if (len > 0) {
244+
env.GetByteArrayRegion(bytes, 0, len, reinterpret_cast<jbyte*>(&out[0]));
245+
}
246+
247+
// Content-Type if available
248+
jmethodID getContentType = env.GetMethodID(clsConn, "getContentType", "()Ljava/lang/String;");
249+
jstring jct = (jstring) env.CallObjectMethod(conn, getContentType);
250+
if (jct) {
251+
contentType = ArgConverter::jstringToString(jct);
252+
}
253+
254+
if (status == 0) status = 200; // assume OK if not HTTP
255+
return status >= 200 && status < 300 && !out.empty();
256+
} catch (...) {
257+
return false;
258+
}
259+
}
260+
261+
} // namespace tns
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// HMRSupport.h
2+
#pragma once
3+
4+
#include <string>
5+
#include <vector>
6+
#include <v8.h>
7+
8+
namespace tns {
9+
10+
// import.meta.hot support
11+
v8::Local<v8::Object> GetOrCreateHotData(v8::Isolate* isolate, const std::string& key);
12+
void RegisterHotAccept(v8::Isolate* isolate, const std::string& key, v8::Local<v8::Function> cb);
13+
void RegisterHotDispose(v8::Isolate* isolate, const std::string& key, v8::Local<v8::Function> cb);
14+
std::vector<v8::Local<v8::Function>> GetHotAcceptCallbacks(v8::Isolate* isolate, const std::string& key);
15+
std::vector<v8::Local<v8::Function>> GetHotDisposeCallbacks(v8::Isolate* isolate, const std::string& key);
16+
void InitializeImportMetaHot(v8::Isolate* isolate,
17+
v8::Local<v8::Context> context,
18+
v8::Local<v8::Object> importMeta,
19+
const std::string& modulePath);
20+
21+
// Dev HTTP loader helpers
22+
std::string CanonicalizeHttpUrlKey(const std::string& url);
23+
bool HttpFetchText(const std::string& url, std::string& out, std::string& contentType, int& status);
24+
25+
} // namespace tns

0 commit comments

Comments
 (0)