@mikrojs/native 0.0.7
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/CMakeLists.txt +198 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/cmake/mikrojs_bytecode.cmake +146 -0
- package/cmake.js +22 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +132 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/include/byteorder_apple.h +11 -0
- package/include/byteorder_windows.h +12 -0
- package/include/mikrojs/cbor_helpers.h +24 -0
- package/include/mikrojs/cutils_wrap.h +59 -0
- package/include/mikrojs/errors.h +144 -0
- package/include/mikrojs/mem.h +11 -0
- package/include/mikrojs/mik_color.h +32 -0
- package/include/mikrojs/mikrojs.h +331 -0
- package/include/mikrojs/platform.h +82 -0
- package/include/mikrojs/private.h +281 -0
- package/include/mikrojs/utils.h +125 -0
- package/package.json +100 -0
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/ble/ble.ts +231 -0
- package/runtime/ble/types.ts +194 -0
- package/runtime/ble/uuid.ts +89 -0
- package/runtime/ble/validators.ts +61 -0
- package/runtime/cbor/cbor.ts +1 -0
- package/runtime/cbor/types.ts +8 -0
- package/runtime/console/types.ts +50 -0
- package/runtime/env/env.ts +17 -0
- package/runtime/env/types.ts +12 -0
- package/runtime/format/types.ts +4 -0
- package/runtime/fs/fs.ts +93 -0
- package/runtime/fs/types.ts +92 -0
- package/runtime/globals.d.ts +87 -0
- package/runtime/http/helpers.ts +222 -0
- package/runtime/http/native.ts +151 -0
- package/runtime/http/request.ts +25 -0
- package/runtime/i2c/i2c.ts +35 -0
- package/runtime/i2c/types.ts +55 -0
- package/runtime/inspect/types.ts +10 -0
- package/runtime/internal.d.ts +456 -0
- package/runtime/kv/nvs.ts +17 -0
- package/runtime/kv/rtc.ts +17 -0
- package/runtime/kv/shared.ts +107 -0
- package/runtime/kv/types.ts +150 -0
- package/runtime/neopixel/neopixel.ts +38 -0
- package/runtime/neopixel/types.ts +27 -0
- package/runtime/pin/pin.ts +51 -0
- package/runtime/pin/types.ts +49 -0
- package/runtime/pwm/pwm.ts +32 -0
- package/runtime/pwm/types.ts +29 -0
- package/runtime/reader/reader.ts +167 -0
- package/runtime/reader/types.ts +34 -0
- package/runtime/result/native-result.node-shim.ts +44 -0
- package/runtime/result/result.ts +26 -0
- package/runtime/result/types.ts +60 -0
- package/runtime/schema/schema.ts +321 -0
- package/runtime/schema/types.ts +152 -0
- package/runtime/sleep/sleep.ts +14 -0
- package/runtime/sleep/types.ts +44 -0
- package/runtime/sntp/sntp.ts +54 -0
- package/runtime/sntp/types.ts +38 -0
- package/runtime/spi/spi.ts +31 -0
- package/runtime/spi/types.ts +42 -0
- package/runtime/stdio/stdio.ts +44 -0
- package/runtime/stdio/types.ts +22 -0
- package/runtime/stream/stream.ts +150 -0
- package/runtime/stream/types.ts +47 -0
- package/runtime/sys/sys.ts +90 -0
- package/runtime/sys/types.ts +131 -0
- package/runtime/test/test.ts +595 -0
- package/runtime/test/types.ts +97 -0
- package/runtime/uart/types.ts +75 -0
- package/runtime/uart/uart.ts +51 -0
- package/runtime/wifi/types.ts +156 -0
- package/runtime/wifi/wifi.ts +208 -0
- package/scripts/bundle-runtime.js +149 -0
- package/scripts/compare-minifiers.js +189 -0
- package/scripts/compile-bytecode.sh +38 -0
- package/scripts/copy-prebuild.js +20 -0
- package/scripts/generate-symbol-map.js +146 -0
- package/src/builtins.cpp +82 -0
- package/src/cutils_compat.c +38 -0
- package/src/eval_bytecode.cpp +42 -0
- package/src/fs.cpp +878 -0
- package/src/mem.cpp +63 -0
- package/src/mik_abort.cpp +160 -0
- package/src/mik_app_config.cpp +358 -0
- package/src/mik_cbor.cpp +334 -0
- package/src/mik_color.cpp +46 -0
- package/src/mik_console.cpp +422 -0
- package/src/mik_inspect.cpp +850 -0
- package/src/mik_repl.cpp +1122 -0
- package/src/mik_result.cpp +344 -0
- package/src/mik_stdio.cpp +147 -0
- package/src/mik_sys.cpp +239 -0
- package/src/mik_text_encoding.cpp +443 -0
- package/src/mikrojs.cpp +942 -0
- package/src/modules.cpp +944 -0
- package/src/platform_posix.cpp +134 -0
- package/src/timers.cpp +208 -0
- package/src/utils.cpp +173 -0
package/src/modules.cpp
ADDED
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
|
|
2
|
+
#include <stdio.h>
|
|
3
|
+
#include <string.h>
|
|
4
|
+
#include <sys/stat.h>
|
|
5
|
+
|
|
6
|
+
#include "mikrojs/mikrojs.h"
|
|
7
|
+
#include "mikrojs/private.h"
|
|
8
|
+
#include "mikrojs/utils.h"
|
|
9
|
+
|
|
10
|
+
/* Init callback for bjson-backed modules: sets the default export from private value */
|
|
11
|
+
static int mik__bjson_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
12
|
+
JSValue val = JS_GetModulePrivateValue(ctx, m);
|
|
13
|
+
JS_SetModuleExport(ctx, m, "default", val);
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* Load a .bjson file and create a module with the deserialized value as default export.
|
|
19
|
+
*/
|
|
20
|
+
static JSModuleDef* mik__load_bjson_module(JSContext* ctx, const char* module_name,
|
|
21
|
+
const char* bjson_path) {
|
|
22
|
+
DynBuf dbuf;
|
|
23
|
+
mik_dbuf_init(ctx, &dbuf);
|
|
24
|
+
if (mik__load_file(ctx, &dbuf, bjson_path) != 0) {
|
|
25
|
+
dbuf_free(&dbuf);
|
|
26
|
+
JS_ThrowTypeError(ctx, "Failed to resolve module specifier '%s'", module_name);
|
|
27
|
+
return NULL;
|
|
28
|
+
}
|
|
29
|
+
JSValue val = JS_ReadObject(ctx, dbuf.buf, dbuf.size, 0);
|
|
30
|
+
dbuf_free(&dbuf);
|
|
31
|
+
if (JS_IsException(val)) {
|
|
32
|
+
JS_FreeValue(ctx, val);
|
|
33
|
+
return NULL;
|
|
34
|
+
}
|
|
35
|
+
JSModuleDef* m = JS_NewCModule(ctx, module_name, mik__bjson_module_init);
|
|
36
|
+
if (!m) {
|
|
37
|
+
JS_FreeValue(ctx, val);
|
|
38
|
+
return NULL;
|
|
39
|
+
}
|
|
40
|
+
JS_AddModuleExport(ctx, m, "default");
|
|
41
|
+
JS_SetModulePrivateValue(ctx, m, val);
|
|
42
|
+
return m;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
* Load a pre-compiled bytecode module (.bjs) from a filesystem path.
|
|
47
|
+
* Handles file I/O, deserialization, module resolution, and import.meta setup.
|
|
48
|
+
*/
|
|
49
|
+
static JSModuleDef* mik__load_bjs_module(JSContext* ctx, const char* module_name,
|
|
50
|
+
const char* bjs_path) {
|
|
51
|
+
DynBuf dbuf;
|
|
52
|
+
mik_dbuf_init(ctx, &dbuf);
|
|
53
|
+
if (mik__load_file(ctx, &dbuf, bjs_path) != 0) {
|
|
54
|
+
dbuf_free(&dbuf);
|
|
55
|
+
JS_ThrowTypeError(ctx, "Failed to resolve module specifier '%s'", module_name);
|
|
56
|
+
return NULL;
|
|
57
|
+
}
|
|
58
|
+
JSValue func_val = JS_ReadObject(ctx, dbuf.buf, dbuf.size, JS_READ_OBJ_BYTECODE);
|
|
59
|
+
dbuf_free(&dbuf);
|
|
60
|
+
if (JS_IsException(func_val)) {
|
|
61
|
+
JS_FreeValue(ctx, func_val);
|
|
62
|
+
return NULL;
|
|
63
|
+
}
|
|
64
|
+
if (JS_ResolveModule(ctx, func_val) < 0) {
|
|
65
|
+
JS_FreeValue(ctx, func_val);
|
|
66
|
+
return NULL;
|
|
67
|
+
}
|
|
68
|
+
js_module_set_import_meta(ctx, func_val, true, false);
|
|
69
|
+
JSModuleDef* m = static_cast<JSModuleDef*>(JS_VALUE_GET_PTR(func_val));
|
|
70
|
+
JS_FreeValue(ctx, func_val);
|
|
71
|
+
return m;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Load a module from the filesystem. Heap-allocates path buffers to keep
|
|
75
|
+
* the stack frame small during recursive .bjs deserialization. */
|
|
76
|
+
static JSModuleDef* mik_module_load_from_fs(JSContext* ctx, const char* module_name) {
|
|
77
|
+
static const char json_tpl_start[] = "export default JSON.parse(`";
|
|
78
|
+
static const char json_tpl_end[] = "`);";
|
|
79
|
+
static const char txt_tpl_start[] = "export default `";
|
|
80
|
+
static const char txt_tpl_end[] = "`;";
|
|
81
|
+
|
|
82
|
+
JSModuleDef* m;
|
|
83
|
+
JSValue func_val;
|
|
84
|
+
DynBuf dbuf;
|
|
85
|
+
|
|
86
|
+
char* fs_path = static_cast<char*>(js_malloc(ctx, PATH_MAX));
|
|
87
|
+
if (!fs_path) return NULL;
|
|
88
|
+
mik__resolve_fs_path(ctx, module_name, fs_path, PATH_MAX);
|
|
89
|
+
|
|
90
|
+
/* Prefer pre-compiled bytecode (.bjs) over source (.js) when available */
|
|
91
|
+
if (js__has_suffix(module_name, ".js")) {
|
|
92
|
+
size_t len = strlen(fs_path);
|
|
93
|
+
char* bjs_path = static_cast<char*>(js_malloc(ctx, len + 2));
|
|
94
|
+
if (bjs_path) {
|
|
95
|
+
memcpy(bjs_path, fs_path, len - 3);
|
|
96
|
+
strcpy(bjs_path + len - 3, ".bjs");
|
|
97
|
+
struct stat st;
|
|
98
|
+
bool found = stat(bjs_path, &st) == 0;
|
|
99
|
+
if (found) {
|
|
100
|
+
js_free(ctx, fs_path);
|
|
101
|
+
JSModuleDef* result = mik__load_bjs_module(ctx, module_name, bjs_path);
|
|
102
|
+
js_free(ctx, bjs_path);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
js_free(ctx, bjs_path);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Pre-compiled bytecode modules (.bjs) — compiled on host via qjsc -b */
|
|
110
|
+
if (js__has_suffix(module_name, ".bjs")) {
|
|
111
|
+
JSModuleDef* result = mik__load_bjs_module(ctx, module_name, fs_path);
|
|
112
|
+
js_free(ctx, fs_path);
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Prefer pre-compiled bjson over source .json when available */
|
|
117
|
+
if (js__has_suffix(module_name, ".json")) {
|
|
118
|
+
size_t len = strlen(fs_path);
|
|
119
|
+
char* bjson_path = static_cast<char*>(js_malloc(ctx, len + 2));
|
|
120
|
+
if (bjson_path) {
|
|
121
|
+
memcpy(bjson_path, fs_path, len - 5);
|
|
122
|
+
strcpy(bjson_path + len - 5, ".bjson");
|
|
123
|
+
struct stat st;
|
|
124
|
+
bool found = stat(bjson_path, &st) == 0;
|
|
125
|
+
if (found) {
|
|
126
|
+
js_free(ctx, fs_path);
|
|
127
|
+
JSModuleDef* result = mik__load_bjson_module(ctx, module_name, bjson_path);
|
|
128
|
+
js_free(ctx, bjson_path);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
js_free(ctx, bjson_path);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Direct .bjson import */
|
|
136
|
+
if (js__has_suffix(module_name, ".bjson")) {
|
|
137
|
+
JSModuleDef* result = mik__load_bjson_module(ctx, module_name, fs_path);
|
|
138
|
+
js_free(ctx, fs_path);
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
mik_dbuf_init(ctx, &dbuf);
|
|
143
|
+
|
|
144
|
+
int is_json = js__has_suffix(module_name, ".json");
|
|
145
|
+
int is_txt = js__has_suffix(module_name, ".txt");
|
|
146
|
+
|
|
147
|
+
/* Wrap JSON/text imports in a template that default-exports the content */
|
|
148
|
+
if (is_json) {
|
|
149
|
+
dbuf_put(&dbuf, (const uint8_t*)json_tpl_start, strlen(json_tpl_start));
|
|
150
|
+
} else if (is_txt) {
|
|
151
|
+
dbuf_put(&dbuf, (const uint8_t*)txt_tpl_start, strlen(txt_tpl_start));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (mik__load_file(ctx, &dbuf, fs_path) != 0) {
|
|
155
|
+
js_free(ctx, fs_path);
|
|
156
|
+
dbuf_free(&dbuf);
|
|
157
|
+
JS_ThrowTypeError(ctx, "Failed to resolve module specifier '%s'", module_name);
|
|
158
|
+
return NULL;
|
|
159
|
+
}
|
|
160
|
+
js_free(ctx, fs_path);
|
|
161
|
+
|
|
162
|
+
if (is_json) {
|
|
163
|
+
dbuf_put(&dbuf, (const uint8_t*)json_tpl_end, strlen(json_tpl_end));
|
|
164
|
+
} else if (is_txt) {
|
|
165
|
+
dbuf_put(&dbuf, (const uint8_t*)txt_tpl_end, strlen(txt_tpl_end));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Add null termination, required by JS_Eval. */
|
|
169
|
+
dbuf_putc(&dbuf, '\0');
|
|
170
|
+
|
|
171
|
+
/* Run preprocessor on source files (e.g. TypeScript type stripping) */
|
|
172
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
173
|
+
char* pp_source = NULL;
|
|
174
|
+
size_t pp_len = 0;
|
|
175
|
+
if (!is_json && !is_txt && mik_rt && mik_rt->preprocess_fn) {
|
|
176
|
+
pp_source = mik_rt->preprocess_fn(module_name, (char*)dbuf.buf, dbuf.size - 1, &pp_len,
|
|
177
|
+
mik_rt->preprocess_opaque);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const char* eval_source = pp_source ? pp_source : (char*)dbuf.buf;
|
|
181
|
+
size_t eval_len = pp_source ? pp_len : dbuf.size - 1;
|
|
182
|
+
|
|
183
|
+
/* compile JS the module */
|
|
184
|
+
func_val = JS_Eval(ctx, eval_source, eval_len, module_name,
|
|
185
|
+
JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
|
|
186
|
+
free(pp_source);
|
|
187
|
+
dbuf_free(&dbuf);
|
|
188
|
+
if (JS_IsException(func_val)) {
|
|
189
|
+
JS_FreeValue(ctx, func_val);
|
|
190
|
+
return NULL;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
js_module_set_import_meta(ctx, func_val, true, false);
|
|
194
|
+
/* the module is already referenced, so we must free it */
|
|
195
|
+
m = static_cast<JSModuleDef*>(JS_VALUE_GET_PTR(func_val));
|
|
196
|
+
JS_FreeValue(ctx, func_val);
|
|
197
|
+
|
|
198
|
+
return m;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
static JSModuleDef* mik_module_loader_inner(JSContext* ctx, const char* module_name,
|
|
202
|
+
void* opaque) {
|
|
203
|
+
static const char mikrojs_prefix[] = "mikrojs/";
|
|
204
|
+
|
|
205
|
+
/* Virtual modules take priority over builtins — allows host-side JS to
|
|
206
|
+
* override any native:* C module (e.g. mocking device modules for dev). */
|
|
207
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
208
|
+
if (mik_rt) {
|
|
209
|
+
auto it = mik_rt->virtual_modules.find(module_name);
|
|
210
|
+
if (it != mik_rt->virtual_modules.end()) {
|
|
211
|
+
JSValue func_val = JS_Eval(ctx, it->second.c_str(), it->second.size(), module_name,
|
|
212
|
+
JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
|
|
213
|
+
if (JS_IsException(func_val)) {
|
|
214
|
+
JS_FreeValue(ctx, func_val);
|
|
215
|
+
return NULL;
|
|
216
|
+
}
|
|
217
|
+
JSModuleDef* m = static_cast<JSModuleDef*>(JS_VALUE_GET_PTR(func_val));
|
|
218
|
+
JS_FreeValue(ctx, func_val);
|
|
219
|
+
return m;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* Lazy init: check the global native module registry for native:* modules.
|
|
224
|
+
* These are registered via MIK_REGISTER_MODULE() at link time but only
|
|
225
|
+
* initialized on first import. */
|
|
226
|
+
if (strncmp("native:", module_name, 7) == 0) {
|
|
227
|
+
for (mik_module_desc_t* d = mik__module_registry_head; d != nullptr; d = d->next) {
|
|
228
|
+
if (strcmp(d->name, module_name) == 0) {
|
|
229
|
+
JSModuleDef* lazy_m = d->init(ctx);
|
|
230
|
+
if (lazy_m && mik_rt && d->consume) {
|
|
231
|
+
MIK_RegisterLoopConsumer(mik_rt, d->consume, d->destroy);
|
|
232
|
+
}
|
|
233
|
+
return lazy_m;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/* Native module not found — this is a device-only module that isn't
|
|
237
|
+
* available in the current build (e.g. running on desktop). */
|
|
238
|
+
JS_ThrowReferenceError(ctx, "Native module '%s' is not available", module_name);
|
|
239
|
+
return nullptr;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (strncmp(mikrojs_prefix, module_name, strlen(mikrojs_prefix)) == 0) {
|
|
243
|
+
JSModuleDef* builtin_m = mik__load_builtin(ctx, module_name);
|
|
244
|
+
if (builtin_m) return builtin_m;
|
|
245
|
+
/* mik__load_builtin returns NULL either because the module isn't in
|
|
246
|
+
* the table, or because deserialization failed (e.g. stack overflow
|
|
247
|
+
* from deeply nested .bjs loading). If deserialization failed, the
|
|
248
|
+
* real exception is already on ctx — don't replace it. */
|
|
249
|
+
JSValue pending = JS_GetException(ctx);
|
|
250
|
+
if (!JS_IsNull(pending)) {
|
|
251
|
+
JS_Throw(ctx, pending);
|
|
252
|
+
} else {
|
|
253
|
+
JS_ThrowReferenceError(ctx, "Builtin module '%s' is not available in this build",
|
|
254
|
+
module_name);
|
|
255
|
+
}
|
|
256
|
+
return NULL;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* Check external builtins for board/driver packages (e.g. "@mikrojs/your-driver").
|
|
260
|
+
* These are registered via MIK_REGISTER_BUILTIN() and resolved by package name
|
|
261
|
+
* before falling through to filesystem resolution. */
|
|
262
|
+
{
|
|
263
|
+
JSModuleDef* ext_m = mik__load_builtin(ctx, module_name);
|
|
264
|
+
if (ext_m) return ext_m;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return mik_module_load_from_fs(ctx, module_name);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
JSModuleDef* mik_module_loader(JSContext* ctx, const char* module_name, void* opaque) {
|
|
271
|
+
/* Optional trace log for diagnosing module-load failures. When
|
|
272
|
+
* MIK_DEBUG_MODULES is set in the environment, every module load
|
|
273
|
+
* attempt is logged to stderr along with the outcome. Useful when
|
|
274
|
+
* tracking down "Error: null" issues where a loader path returns
|
|
275
|
+
* NULL without setting a proper JS exception. */
|
|
276
|
+
static const char* debug_env = getenv("MIK_DEBUG_MODULES");
|
|
277
|
+
bool debug = debug_env != nullptr && debug_env[0] != '\0';
|
|
278
|
+
if (debug) {
|
|
279
|
+
fprintf(stderr, "[mik-modules] load: %s\n", module_name);
|
|
280
|
+
fflush(stderr);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
284
|
+
if (mik_rt == nullptr || !mik_rt->profile_enabled) {
|
|
285
|
+
JSModuleDef* result = mik_module_loader_inner(ctx, module_name, opaque);
|
|
286
|
+
if (debug) {
|
|
287
|
+
if (result == nullptr) {
|
|
288
|
+
JSValue exc = JS_GetException(ctx);
|
|
289
|
+
bool has_exc = !JS_IsNull(exc) && !JS_IsUndefined(exc);
|
|
290
|
+
fprintf(stderr, "[mik-modules] FAIL: %s (exception=%s)\n", module_name,
|
|
291
|
+
has_exc ? "set" : "NULL");
|
|
292
|
+
/* Re-throw so the caller still sees the exception. */
|
|
293
|
+
if (has_exc) JS_Throw(ctx, exc);
|
|
294
|
+
else JS_FreeValue(ctx, exc);
|
|
295
|
+
} else {
|
|
296
|
+
fprintf(stderr, "[mik-modules] ok: %s\n", module_name);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
/* Profiling path: snapshot QuickJS malloc_size before and after the
|
|
302
|
+
* inner loader runs, then record the *exclusive* delta — i.e. the
|
|
303
|
+
* cost of loading THIS module's bytecode/source and resolving its
|
|
304
|
+
* imports, not counting whatever its imports already contributed.
|
|
305
|
+
*
|
|
306
|
+
* Why exclusive: mik_module_loader is called recursively. When module
|
|
307
|
+
* A imports B which imports C, the call tree is
|
|
308
|
+
* loader(A) → inner(A) → loader(B) → inner(B) → loader(C) → ...
|
|
309
|
+
* Subtracting children's deltas gives the cost of THIS module alone.
|
|
310
|
+
*
|
|
311
|
+
* Caveat: this measures the load/compilation cost, not the full
|
|
312
|
+
* steady-state cost. Linking (import/export binding) and top-level
|
|
313
|
+
* evaluation happen later inside JS_EvalFunction where we have no
|
|
314
|
+
* per-module hook. The profile total will be lower than actual
|
|
315
|
+
* QuickJS heap usage by the amount those phases allocate. */
|
|
316
|
+
size_t entries_before = mik_rt->profile_entries.size();
|
|
317
|
+
JSMemoryUsage before;
|
|
318
|
+
JS_ComputeMemoryUsage(JS_GetRuntime(ctx), &before);
|
|
319
|
+
JSModuleDef* m = mik_module_loader_inner(ctx, module_name, opaque);
|
|
320
|
+
if (debug) {
|
|
321
|
+
if (m == nullptr) {
|
|
322
|
+
JSValue exc = JS_GetException(ctx);
|
|
323
|
+
bool has_exc = !JS_IsNull(exc) && !JS_IsUndefined(exc);
|
|
324
|
+
fprintf(stderr, "[mik-modules] FAIL: %s (exception=%s)\n", module_name,
|
|
325
|
+
has_exc ? "set" : "NULL");
|
|
326
|
+
if (has_exc) JS_Throw(ctx, exc);
|
|
327
|
+
else JS_FreeValue(ctx, exc);
|
|
328
|
+
} else {
|
|
329
|
+
fprintf(stderr, "[mik-modules] ok: %s\n", module_name);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
JSMemoryUsage after;
|
|
333
|
+
JS_ComputeMemoryUsage(JS_GetRuntime(ctx), &after);
|
|
334
|
+
|
|
335
|
+
size_t total_delta = after.malloc_size > before.malloc_size
|
|
336
|
+
? (size_t)(after.malloc_size - before.malloc_size)
|
|
337
|
+
: 0;
|
|
338
|
+
size_t children_delta = 0;
|
|
339
|
+
for (size_t i = entries_before; i < mik_rt->profile_entries.size(); i++) {
|
|
340
|
+
children_delta += mik_rt->profile_entries[i].delta_bytes;
|
|
341
|
+
}
|
|
342
|
+
size_t exclusive_delta = total_delta > children_delta ? total_delta - children_delta : 0;
|
|
343
|
+
|
|
344
|
+
MIKProfileEntry entry = {};
|
|
345
|
+
size_t name_len = strlen(module_name);
|
|
346
|
+
if (name_len >= MIK_PROFILE_NAME_MAX) {
|
|
347
|
+
name_len = MIK_PROFILE_NAME_MAX - 1;
|
|
348
|
+
}
|
|
349
|
+
memcpy(entry.name, module_name, name_len);
|
|
350
|
+
entry.name[name_len] = '\0';
|
|
351
|
+
entry.delta_bytes = exclusive_delta;
|
|
352
|
+
entry.order = mik_rt->profile_entries.size();
|
|
353
|
+
if (mik_rt->profile_entries.size() < mik_rt->profile_entries.capacity()) {
|
|
354
|
+
mik_rt->profile_entries.push_back(entry);
|
|
355
|
+
}
|
|
356
|
+
return m;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#define MIK__PATHSEP_POSIX '/'
|
|
360
|
+
#define MIK__PATHSEP '/'
|
|
361
|
+
#define MIK__PATHSEP_STR "/"
|
|
362
|
+
|
|
363
|
+
/* import.meta.unload(specifier) implementation. Resolves `specifier` via
|
|
364
|
+
* the standard normalizer using the captured base path (the owning
|
|
365
|
+
* module's logical path, stripped of the `file://` prefix), evicts the
|
|
366
|
+
* resulting module + orphaned deps via mik__unload_module, then runs a
|
|
367
|
+
* GC pass so the caller's next memoryUsage() snapshot reflects the free. */
|
|
368
|
+
static JSValue mik__import_meta_unload(JSContext* ctx, JSValueConst this_val,
|
|
369
|
+
int argc, JSValueConst* argv,
|
|
370
|
+
int magic, JSValueConst* func_data) {
|
|
371
|
+
(void)magic;
|
|
372
|
+
(void)this_val;
|
|
373
|
+
if (argc < 1 || !JS_IsString(argv[0])) {
|
|
374
|
+
return JS_ThrowTypeError(ctx, "import.meta.unload: specifier must be a string");
|
|
375
|
+
}
|
|
376
|
+
const char* specifier = JS_ToCString(ctx, argv[0]);
|
|
377
|
+
if (!specifier) return JS_EXCEPTION;
|
|
378
|
+
const char* base = JS_ToCString(ctx, func_data[0]);
|
|
379
|
+
if (!base) {
|
|
380
|
+
JS_FreeCString(ctx, specifier);
|
|
381
|
+
return JS_EXCEPTION;
|
|
382
|
+
}
|
|
383
|
+
char* normalized = mik_module_normalizer(ctx, base, specifier, nullptr);
|
|
384
|
+
JS_FreeCString(ctx, specifier);
|
|
385
|
+
JS_FreeCString(ctx, base);
|
|
386
|
+
if (!normalized) return JS_EXCEPTION;
|
|
387
|
+
|
|
388
|
+
int rv = mik__unload_module(ctx, normalized);
|
|
389
|
+
js_free(ctx, normalized);
|
|
390
|
+
if (rv < 0) return JS_EXCEPTION;
|
|
391
|
+
|
|
392
|
+
JS_RunGC(JS_GetRuntime(ctx));
|
|
393
|
+
return JS_NewInt32(ctx, rv);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
int js_module_set_import_meta(JSContext* ctx, JSValue func_val, bool use_realpath, bool is_main) {
|
|
397
|
+
JSModuleDef* m;
|
|
398
|
+
char buf[PATH_MAX + 16] = {0};
|
|
399
|
+
JSValue meta_obj;
|
|
400
|
+
JSAtom module_name_atom;
|
|
401
|
+
const char* module_name;
|
|
402
|
+
char module_dirname[PATH_MAX] = {0};
|
|
403
|
+
char module_basename[PATH_MAX] = {0};
|
|
404
|
+
|
|
405
|
+
CHECK_EQ(JS_VALUE_GET_TAG(func_val), JS_TAG_MODULE);
|
|
406
|
+
m = static_cast<JSModuleDef*>(JS_VALUE_GET_PTR(func_val));
|
|
407
|
+
|
|
408
|
+
module_name_atom = JS_GetModuleName(ctx, m);
|
|
409
|
+
module_name = JS_AtomToCString(ctx, module_name_atom);
|
|
410
|
+
JS_FreeAtom(ctx, module_name_atom);
|
|
411
|
+
if (!module_name) {
|
|
412
|
+
return -1;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (use_realpath) {
|
|
416
|
+
/* On LittleFS paths are already absolute, so construct file:// URL directly */
|
|
417
|
+
js__pstrcpy(buf, sizeof(buf), "file://");
|
|
418
|
+
js__pstrcat(buf, sizeof(buf), module_name);
|
|
419
|
+
|
|
420
|
+
/* Extract dirname and basename from the absolute path */
|
|
421
|
+
const char* start = buf + 7; /* skip file:// */
|
|
422
|
+
const char* p = strrchr(start, MIK__PATHSEP);
|
|
423
|
+
if (p) {
|
|
424
|
+
strncpy(module_dirname, start, p - start);
|
|
425
|
+
strcpy(module_basename, p + 1);
|
|
426
|
+
} else {
|
|
427
|
+
module_dirname[0] = '\0';
|
|
428
|
+
strcpy(module_basename, start);
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
js__pstrcpy(buf, sizeof(buf), module_name);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
JS_FreeCString(ctx, module_name);
|
|
435
|
+
|
|
436
|
+
meta_obj = JS_GetImportMeta(ctx, m);
|
|
437
|
+
if (JS_IsException(meta_obj)) {
|
|
438
|
+
return -1;
|
|
439
|
+
}
|
|
440
|
+
JS_DefinePropertyValueStr(ctx, meta_obj, "url", JS_NewString(ctx, buf), JS_PROP_C_W_E);
|
|
441
|
+
JS_DefinePropertyValueStr(ctx, meta_obj, "main", JS_NewBool(ctx, is_main), JS_PROP_C_W_E);
|
|
442
|
+
if (use_realpath) {
|
|
443
|
+
JS_DefinePropertyValueStr(ctx, meta_obj, "dirname", JS_NewString(ctx, module_dirname),
|
|
444
|
+
JS_PROP_C_W_E);
|
|
445
|
+
JS_DefinePropertyValueStr(ctx, meta_obj, "basename", JS_NewString(ctx, module_basename),
|
|
446
|
+
JS_PROP_C_W_E);
|
|
447
|
+
JS_DefinePropertyValueStr(ctx, meta_obj, "path", JS_NewString(ctx, buf + 7),
|
|
448
|
+
JS_PROP_C_W_E);
|
|
449
|
+
}
|
|
450
|
+
/* Set import.meta.env from cached frozen env object. */
|
|
451
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
452
|
+
if (mik_rt && !JS_IsUndefined(mik_rt->env_obj)) {
|
|
453
|
+
JS_DefinePropertyValueStr(ctx, meta_obj, "env", JS_DupValue(ctx, mik_rt->env_obj),
|
|
454
|
+
JS_PROP_C_W_E);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* Attach import.meta.unload(specifier). Needs the owning module's
|
|
458
|
+
* logical path (not URL) as the normalizer base. Only exposed for
|
|
459
|
+
* file-backed modules; synthetic modules don't have a stable path. */
|
|
460
|
+
if (use_realpath) {
|
|
461
|
+
JSValue base_str = JS_NewString(ctx, buf + 7 /* strip "file://" */);
|
|
462
|
+
JSValue unload_fn = JS_NewCFunctionData(ctx, mik__import_meta_unload, 1, 0, 1, &base_str);
|
|
463
|
+
JS_FreeValue(ctx, base_str);
|
|
464
|
+
JS_DefinePropertyValueStr(ctx, meta_obj, "unload", unload_fn, JS_PROP_C_W_E);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
JS_FreeValue(ctx, meta_obj);
|
|
468
|
+
return 0;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/*
|
|
472
|
+
* Resolve an export condition value to a string path.
|
|
473
|
+
* Handles: string literals, and objects with "import" / "default" conditions.
|
|
474
|
+
*/
|
|
475
|
+
static const char* mik__resolve_export_condition(JSContext* ctx, JSValue exp) {
|
|
476
|
+
if (JS_IsString(exp)) {
|
|
477
|
+
return JS_ToCString(ctx, exp);
|
|
478
|
+
}
|
|
479
|
+
if (!JS_IsObject(exp)) {
|
|
480
|
+
return NULL;
|
|
481
|
+
}
|
|
482
|
+
static const char* conditions[] = {"import", "default"};
|
|
483
|
+
for (size_t i = 0; i < countof(conditions); i++) {
|
|
484
|
+
JSValue val = JS_GetPropertyStr(ctx, exp, conditions[i]);
|
|
485
|
+
if (!JS_IsUndefined(val) && !JS_IsException(val)) {
|
|
486
|
+
const char* result = mik__resolve_export_condition(ctx, val);
|
|
487
|
+
JS_FreeValue(ctx, val);
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
490
|
+
JS_FreeValue(ctx, val);
|
|
491
|
+
}
|
|
492
|
+
return NULL;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/*
|
|
496
|
+
* Given a parsed package.json (as JSValue) and a subpath (e.g. "." or "./utils"),
|
|
497
|
+
* resolve the entry point file path relative to the package directory.
|
|
498
|
+
*
|
|
499
|
+
* TODO: check publishConfig.exports before exports
|
|
500
|
+
*/
|
|
501
|
+
static const char* mik__resolve_pkg_entry(JSContext* ctx, JSValue pkg_json, const char* subpath) {
|
|
502
|
+
JSValue exports = JS_GetPropertyStr(ctx, pkg_json, "exports");
|
|
503
|
+
|
|
504
|
+
if (JS_IsUndefined(exports)) {
|
|
505
|
+
JS_FreeValue(ctx, exports);
|
|
506
|
+
/* No exports field — fall back to "main", then "./index.js" */
|
|
507
|
+
JSValue main_val = JS_GetPropertyStr(ctx, pkg_json, "main");
|
|
508
|
+
if (JS_IsString(main_val)) {
|
|
509
|
+
const char* result = JS_ToCString(ctx, main_val);
|
|
510
|
+
JS_FreeValue(ctx, main_val);
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
513
|
+
JS_FreeValue(ctx, main_val);
|
|
514
|
+
return js_strdup(ctx, "./index.js");
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/* exports is a plain string — use it directly */
|
|
518
|
+
if (JS_IsString(exports)) {
|
|
519
|
+
const char* result = JS_ToCString(ctx, exports);
|
|
520
|
+
JS_FreeValue(ctx, exports);
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/* exports is an object — look up the subpath */
|
|
525
|
+
if (JS_IsObject(exports)) {
|
|
526
|
+
JSValue subpath_val = JS_GetPropertyStr(ctx, exports, subpath);
|
|
527
|
+
if (!JS_IsUndefined(subpath_val) && !JS_IsException(subpath_val)) {
|
|
528
|
+
const char* result = mik__resolve_export_condition(ctx, subpath_val);
|
|
529
|
+
JS_FreeValue(ctx, subpath_val);
|
|
530
|
+
JS_FreeValue(ctx, exports);
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
JS_FreeValue(ctx, subpath_val);
|
|
534
|
+
|
|
535
|
+
/* No subpath key found — when resolving ".", treat the exports object
|
|
536
|
+
* itself as a conditions object. This handles the common shorthand where
|
|
537
|
+
* exports is {"default": "./index.js"} instead of {".": {"default": ...}} */
|
|
538
|
+
if (strcmp(subpath, ".") == 0) {
|
|
539
|
+
const char* result = mik__resolve_export_condition(ctx, exports);
|
|
540
|
+
JS_FreeValue(ctx, exports);
|
|
541
|
+
return result;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
JS_FreeValue(ctx, exports);
|
|
546
|
+
return NULL;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/*
|
|
550
|
+
* Parse a bare specifier into package name (+ length) and subpath.
|
|
551
|
+
* E.g. "@scope/pkg/utils" -> pkg_name="@scope/pkg", subpath="./utils"
|
|
552
|
+
* "lodash" -> pkg_name="lodash", subpath="."
|
|
553
|
+
*/
|
|
554
|
+
struct MIKPkgSpecifier {
|
|
555
|
+
const char* pkg_name;
|
|
556
|
+
int pkg_name_len;
|
|
557
|
+
const char* subpath;
|
|
558
|
+
char subpath_buf[PATH_MAX];
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
static bool mik__parse_pkg_specifier(const char* specifier, MIKPkgSpecifier* out) {
|
|
562
|
+
out->pkg_name = specifier;
|
|
563
|
+
|
|
564
|
+
const char* split = NULL;
|
|
565
|
+
if (specifier[0] == '@') {
|
|
566
|
+
const char* first_slash = strchr(specifier, '/');
|
|
567
|
+
if (!first_slash) {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
split = strchr(first_slash + 1, '/');
|
|
571
|
+
} else {
|
|
572
|
+
split = strchr(specifier, '/');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (split) {
|
|
576
|
+
out->pkg_name_len = (int)(split - specifier);
|
|
577
|
+
snprintf(out->subpath_buf, sizeof(out->subpath_buf), "./%s", split + 1);
|
|
578
|
+
out->subpath = out->subpath_buf;
|
|
579
|
+
} else {
|
|
580
|
+
out->pkg_name_len = (int)strlen(specifier);
|
|
581
|
+
out->subpath = ".";
|
|
582
|
+
}
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/*
|
|
587
|
+
* Try to resolve a package entry from a specific node_modules directory.
|
|
588
|
+
* Returns a js_malloc'd absolute path on success, NULL on failure.
|
|
589
|
+
*/
|
|
590
|
+
static char* mik__try_resolve_pkg(JSContext* ctx, const char* dir, const MIKPkgSpecifier* spec) {
|
|
591
|
+
char pkg_json_path[PATH_MAX];
|
|
592
|
+
snprintf(pkg_json_path, sizeof(pkg_json_path), "%s/node_modules/%.*s/package.json", dir,
|
|
593
|
+
spec->pkg_name_len, spec->pkg_name);
|
|
594
|
+
|
|
595
|
+
/* Resolve logical path to filesystem path for file I/O */
|
|
596
|
+
char pkg_json_fs_path[PATH_MAX];
|
|
597
|
+
mik__resolve_fs_path(ctx, pkg_json_path, pkg_json_fs_path, sizeof(pkg_json_fs_path));
|
|
598
|
+
|
|
599
|
+
JSValue pkg_json = JS_UNDEFINED;
|
|
600
|
+
|
|
601
|
+
/* Prefer pre-compiled .bjson over source .json when available */
|
|
602
|
+
char bjson_fs_path[PATH_MAX];
|
|
603
|
+
size_t len = strlen(pkg_json_fs_path);
|
|
604
|
+
memcpy(bjson_fs_path, pkg_json_fs_path, len - 5); /* strip ".json" */
|
|
605
|
+
strcpy(bjson_fs_path + len - 5, ".bjson");
|
|
606
|
+
|
|
607
|
+
DynBuf dbuf;
|
|
608
|
+
struct stat st;
|
|
609
|
+
if (stat(bjson_fs_path, &st) == 0) {
|
|
610
|
+
mik_dbuf_init(ctx, &dbuf);
|
|
611
|
+
if (mik__load_file(ctx, &dbuf, bjson_fs_path) == 0) {
|
|
612
|
+
pkg_json = JS_ReadObject(ctx, dbuf.buf, dbuf.size, 0);
|
|
613
|
+
}
|
|
614
|
+
dbuf_free(&dbuf);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* Fall back to plain JSON */
|
|
618
|
+
if (JS_IsUndefined(pkg_json)) {
|
|
619
|
+
FILE* f = fopen(pkg_json_fs_path, "r");
|
|
620
|
+
if (!f) {
|
|
621
|
+
return NULL;
|
|
622
|
+
}
|
|
623
|
+
mik_dbuf_init(ctx, &dbuf);
|
|
624
|
+
char buf[256];
|
|
625
|
+
size_t n;
|
|
626
|
+
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
|
|
627
|
+
dbuf_put(&dbuf, (const uint8_t*)buf, n);
|
|
628
|
+
}
|
|
629
|
+
fclose(f);
|
|
630
|
+
dbuf_putc(&dbuf, '\0');
|
|
631
|
+
pkg_json = JS_ParseJSON(ctx, (const char*)dbuf.buf, dbuf.size - 1, pkg_json_path);
|
|
632
|
+
dbuf_free(&dbuf);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (JS_IsException(pkg_json)) {
|
|
636
|
+
JS_FreeValue(ctx, pkg_json);
|
|
637
|
+
JSValue exc = JS_GetException(ctx);
|
|
638
|
+
JS_FreeValue(ctx, exc);
|
|
639
|
+
return NULL;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const char* entry = mik__resolve_pkg_entry(ctx, pkg_json, spec->subpath);
|
|
643
|
+
JS_FreeValue(ctx, pkg_json);
|
|
644
|
+
|
|
645
|
+
if (!entry) {
|
|
646
|
+
return NULL;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const char* entry_path = entry;
|
|
650
|
+
if (entry_path[0] == '.' && entry_path[1] == '/') {
|
|
651
|
+
entry_path += 2;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
char result[PATH_MAX];
|
|
655
|
+
snprintf(result, sizeof(result), "%s/node_modules/%.*s/%s", dir, spec->pkg_name_len,
|
|
656
|
+
spec->pkg_name, entry_path);
|
|
657
|
+
JS_FreeCString(ctx, entry);
|
|
658
|
+
return js_strdup(ctx, result);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/*
|
|
662
|
+
* Resolve a bare package specifier by walking up from base_dir looking for
|
|
663
|
+
* node_modules/<package_name>/package.json, then resolving the entry point.
|
|
664
|
+
*
|
|
665
|
+
* Returns a js_malloc'd absolute path on success, NULL on failure.
|
|
666
|
+
*/
|
|
667
|
+
static char* mik__resolve_node_modules(JSContext* ctx, const char* base_dir,
|
|
668
|
+
const char* specifier) {
|
|
669
|
+
MIKPkgSpecifier spec;
|
|
670
|
+
if (!mik__parse_pkg_specifier(specifier, &spec)) {
|
|
671
|
+
return NULL;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
char dir[PATH_MAX];
|
|
675
|
+
js__pstrcpy(dir, sizeof(dir), base_dir);
|
|
676
|
+
|
|
677
|
+
for (;;) {
|
|
678
|
+
char* resolved = mik__try_resolve_pkg(ctx, dir, &spec);
|
|
679
|
+
if (resolved) {
|
|
680
|
+
return resolved;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
char* last_sep = strrchr(dir, '/');
|
|
684
|
+
if (!last_sep) {
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
if (last_sep == dir) {
|
|
688
|
+
/* At root — try "/" once, then stop */
|
|
689
|
+
if (dir[1] == '\0') {
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
dir[1] = '\0';
|
|
693
|
+
} else {
|
|
694
|
+
*last_sep = '\0';
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return NULL;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/* Anchored names (native:, mikrojs/, @mikrojs/) are never unloaded
|
|
702
|
+
* and don't contribute meaningfully to the import graph for orphan
|
|
703
|
+
* detection. We skip recording edges where the target is anchored. */
|
|
704
|
+
static bool mik__is_anchored_name(const char* name) {
|
|
705
|
+
return strncmp(name, "native:", 7) == 0 || strncmp(name, "mikrojs/", 8) == 0 ||
|
|
706
|
+
strncmp(name, "@mikrojs/", 9) == 0;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/* Record an edge in the module dependency graph: `base` imports `target`.
|
|
710
|
+
* No-ops if the MIKRuntime is absent, base is empty/non-module, or target
|
|
711
|
+
* is anchored. Keyed by normalized names. */
|
|
712
|
+
static void mik__record_import_edge(MIKRuntime* mik_rt, const char* base,
|
|
713
|
+
const char* target) {
|
|
714
|
+
if (!mik_rt || !base || !*base || !target || !*target) return;
|
|
715
|
+
if (mik__is_anchored_name(target)) return;
|
|
716
|
+
std::string b(base), t(target);
|
|
717
|
+
mik_rt->module_imports[b].insert(t);
|
|
718
|
+
mik_rt->module_importers[t].insert(b);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
static char* mik__module_normalizer_impl(JSContext* ctx, const char* base_name,
|
|
722
|
+
const char* name, void* opaque) {
|
|
723
|
+
char *filename, *p;
|
|
724
|
+
const char* r;
|
|
725
|
+
int len;
|
|
726
|
+
|
|
727
|
+
static const char internal_prefix[] = "native:";
|
|
728
|
+
if (strncmp(name, internal_prefix, strlen(internal_prefix)) == 0) {
|
|
729
|
+
// Only built-in modules (native:, mikrojs/, @mikrojs/) may import native: internals
|
|
730
|
+
if (strncmp(base_name, "native:", 7) != 0 && strncmp(base_name, "mikrojs/", 8) != 0 &&
|
|
731
|
+
strncmp(base_name, "@mikrojs/", 9) != 0) {
|
|
732
|
+
JS_ThrowTypeError(ctx, "Failed to resolve module specifier '%s'", name);
|
|
733
|
+
return NULL;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (name[0] != '.' && name[0] != '/') {
|
|
738
|
+
/* Built-in modules pass through unchanged */
|
|
739
|
+
if (strncmp(name, "mikrojs/", 8) == 0 || strncmp(name, "native:", 7) == 0) {
|
|
740
|
+
return js_strdup(ctx, name);
|
|
741
|
+
}
|
|
742
|
+
/* Check if this bare specifier matches an external builtin (board/driver
|
|
743
|
+
* package). If so, pass through unchanged to avoid filesystem resolution. */
|
|
744
|
+
for (mik_ext_builtin_t* p = mik__ext_builtin_head; p != nullptr; p = p->next) {
|
|
745
|
+
if (strcmp(p->name, name) == 0) {
|
|
746
|
+
return js_strdup(ctx, name);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/* Bare specifier — try node_modules resolution.
|
|
750
|
+
* Heap-allocate the dir buffer to keep the normalizer stack frame
|
|
751
|
+
* small during recursive .bjs loading. */
|
|
752
|
+
{
|
|
753
|
+
const char* sep = strrchr(base_name, '/');
|
|
754
|
+
int dir_len = sep ? (int)(sep - base_name) : 0;
|
|
755
|
+
char* base_dir = static_cast<char*>(js_malloc(ctx, dir_len + 2));
|
|
756
|
+
if (base_dir) {
|
|
757
|
+
if (dir_len > 0) {
|
|
758
|
+
memcpy(base_dir, base_name, dir_len);
|
|
759
|
+
base_dir[dir_len] = '\0';
|
|
760
|
+
} else {
|
|
761
|
+
base_dir[0] = '.';
|
|
762
|
+
base_dir[1] = '\0';
|
|
763
|
+
}
|
|
764
|
+
char* resolved = mik__resolve_node_modules(ctx, base_dir, name);
|
|
765
|
+
js_free(ctx, base_dir);
|
|
766
|
+
if (resolved) {
|
|
767
|
+
return resolved;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/* Fall back to returning the name as-is (for built-in prefixes etc.) */
|
|
772
|
+
return js_strdup(ctx, name);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/* Absolute paths are already fully qualified — don't prepend the
|
|
776
|
+
* caller's directory. */
|
|
777
|
+
if (name[0] == MIK__PATHSEP_POSIX) {
|
|
778
|
+
return js_strdup(ctx, name);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const char* sep = strrchr(base_name, MIK__PATHSEP);
|
|
782
|
+
if (sep) {
|
|
783
|
+
len = sep - base_name;
|
|
784
|
+
} else {
|
|
785
|
+
len = 0;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
filename = static_cast<char*>(js_malloc(ctx, len + strlen(name) + 1 + 1));
|
|
789
|
+
if (!filename) {
|
|
790
|
+
return NULL;
|
|
791
|
+
}
|
|
792
|
+
memcpy(filename, base_name, len);
|
|
793
|
+
filename[len] = '\0';
|
|
794
|
+
|
|
795
|
+
/* we only normalize the leading '..' or '.' */
|
|
796
|
+
r = name;
|
|
797
|
+
for (;;) {
|
|
798
|
+
if (r[0] == '.' && r[1] == MIK__PATHSEP_POSIX) {
|
|
799
|
+
r += 2;
|
|
800
|
+
} else if (r[0] == '.' && r[1] == '.' && r[2] == MIK__PATHSEP_POSIX) {
|
|
801
|
+
/* remove the last path element of filename, except if "."
|
|
802
|
+
or ".." */
|
|
803
|
+
if (filename[0] == '\0') {
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
p = strrchr(filename, MIK__PATHSEP);
|
|
807
|
+
if (!p) {
|
|
808
|
+
p = filename;
|
|
809
|
+
} else {
|
|
810
|
+
p++;
|
|
811
|
+
}
|
|
812
|
+
if (!strcmp(p, ".") || !strcmp(p, "..")) {
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
if (p > filename) {
|
|
816
|
+
p--;
|
|
817
|
+
}
|
|
818
|
+
*p = '\0';
|
|
819
|
+
r += 3;
|
|
820
|
+
} else {
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (filename[0] != '\0') {
|
|
825
|
+
strcat(filename, MIK__PATHSEP_STR);
|
|
826
|
+
}
|
|
827
|
+
strcat(filename, r);
|
|
828
|
+
|
|
829
|
+
return filename;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
#undef MIK__PATHSEP
|
|
833
|
+
#undef MIK__PATHSEP_STR
|
|
834
|
+
#undef MIK__PATHSEP_POSIX
|
|
835
|
+
|
|
836
|
+
char* mik_module_normalizer(JSContext* ctx, const char* base_name, const char* name,
|
|
837
|
+
void* opaque) {
|
|
838
|
+
char* normalized = mik__module_normalizer_impl(ctx, base_name, name, opaque);
|
|
839
|
+
if (normalized) {
|
|
840
|
+
mik__record_import_edge(MIK_GetRuntime(ctx), base_name, normalized);
|
|
841
|
+
}
|
|
842
|
+
return normalized;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/* ─── Module unload ─────────────────────────────────────────────────── */
|
|
846
|
+
|
|
847
|
+
/* Mirror of QuickJS's JSModuleStatus enum. The header can't expose the
|
|
848
|
+
* constants (would clash with the internal enum when quickjs.c compiles),
|
|
849
|
+
* so we re-declare the one status value we care about. */
|
|
850
|
+
#define MIK__MODULE_STATUS_EVALUATED 5
|
|
851
|
+
|
|
852
|
+
int mik__unload_module(JSContext* ctx, const char* normalized_name) {
|
|
853
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
854
|
+
if (!mik_rt) {
|
|
855
|
+
JS_ThrowInternalError(ctx, "mik__unload_module requires a MIKRuntime");
|
|
856
|
+
return -1;
|
|
857
|
+
}
|
|
858
|
+
if (mik__is_anchored_name(normalized_name)) {
|
|
859
|
+
JS_ThrowTypeError(ctx, "cannot unload builtin module '%s'", normalized_name);
|
|
860
|
+
return -1;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/* Verify the root module is loaded before we start unwinding. */
|
|
864
|
+
JSAtom root_atom = JS_NewAtom(ctx, normalized_name);
|
|
865
|
+
JSModuleDef* root_m = JS_FindLoadedModule(ctx, root_atom);
|
|
866
|
+
JS_FreeAtom(ctx, root_atom);
|
|
867
|
+
if (!root_m) {
|
|
868
|
+
JS_ThrowTypeError(ctx, "module '%s' is not loaded", normalized_name);
|
|
869
|
+
return -1;
|
|
870
|
+
}
|
|
871
|
+
int root_status = JS_GetModuleStatus(ctx, root_m);
|
|
872
|
+
if (root_status != MIK__MODULE_STATUS_EVALUATED) {
|
|
873
|
+
JS_ThrowTypeError(ctx,
|
|
874
|
+
"cannot unload '%s': module is not evaluated (status=%d)",
|
|
875
|
+
normalized_name, root_status);
|
|
876
|
+
return -1;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/* BFS through the import graph, collecting modules to unload.
|
|
880
|
+
* A dep is scheduled only if every one of its importers is itself
|
|
881
|
+
* being unloaded in this pass (i.e. it becomes an orphan). */
|
|
882
|
+
std::unordered_set<std::string> scheduled;
|
|
883
|
+
std::vector<std::string> order;
|
|
884
|
+
scheduled.insert(normalized_name);
|
|
885
|
+
order.emplace_back(normalized_name);
|
|
886
|
+
|
|
887
|
+
for (size_t i = 0; i < order.size(); i++) {
|
|
888
|
+
const std::string& cur = order[i];
|
|
889
|
+
auto imports_it = mik_rt->module_imports.find(cur);
|
|
890
|
+
if (imports_it == mik_rt->module_imports.end()) continue;
|
|
891
|
+
for (const std::string& dep : imports_it->second) {
|
|
892
|
+
if (scheduled.count(dep)) continue;
|
|
893
|
+
if (mik__is_anchored_name(dep.c_str())) continue;
|
|
894
|
+
|
|
895
|
+
auto importers_it = mik_rt->module_importers.find(dep);
|
|
896
|
+
if (importers_it == mik_rt->module_importers.end()) continue;
|
|
897
|
+
|
|
898
|
+
bool becomes_orphan = true;
|
|
899
|
+
for (const std::string& importer : importers_it->second) {
|
|
900
|
+
if (scheduled.count(importer) == 0) {
|
|
901
|
+
becomes_orphan = false;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (becomes_orphan) {
|
|
906
|
+
scheduled.insert(dep);
|
|
907
|
+
order.emplace_back(dep);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/* Free each scheduled module. Skip any that are gone or aren't in a
|
|
913
|
+
* freeable state. */
|
|
914
|
+
int freed_count = 0;
|
|
915
|
+
for (const std::string& name : order) {
|
|
916
|
+
JSAtom atom = JS_NewAtom(ctx, name.c_str());
|
|
917
|
+
JSModuleDef* m = JS_FindLoadedModule(ctx, atom);
|
|
918
|
+
JS_FreeAtom(ctx, atom);
|
|
919
|
+
if (!m) continue;
|
|
920
|
+
int status = JS_GetModuleStatus(ctx, m);
|
|
921
|
+
if (status != MIK__MODULE_STATUS_EVALUATED) continue;
|
|
922
|
+
JS_FreeModule(ctx, m);
|
|
923
|
+
freed_count++;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/* Scrub the graph: drop unloaded nodes as importers of anything they
|
|
927
|
+
* imported, and remove their own import/importer entries. */
|
|
928
|
+
for (const std::string& name : order) {
|
|
929
|
+
auto imports_it = mik_rt->module_imports.find(name);
|
|
930
|
+
if (imports_it != mik_rt->module_imports.end()) {
|
|
931
|
+
for (const std::string& dep : imports_it->second) {
|
|
932
|
+
auto imp_it = mik_rt->module_importers.find(dep);
|
|
933
|
+
if (imp_it != mik_rt->module_importers.end()) {
|
|
934
|
+
imp_it->second.erase(name);
|
|
935
|
+
if (imp_it->second.empty()) mik_rt->module_importers.erase(imp_it);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
mik_rt->module_imports.erase(imports_it);
|
|
939
|
+
}
|
|
940
|
+
mik_rt->module_importers.erase(name);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return freed_count;
|
|
944
|
+
}
|