@mikrojs/native 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CMakeLists.txt +62 -1
  2. package/dist/index.d.ts +17 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +11 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/runtime/result/native-result.node-shim.d.ts +3 -0
  7. package/dist/runtime/result/native-result.node-shim.d.ts.map +1 -0
  8. package/dist/runtime/result/native-result.node-shim.js +41 -0
  9. package/dist/runtime/result/native-result.node-shim.js.map +1 -0
  10. package/dist/runtime/result/types.d.ts +55 -0
  11. package/dist/runtime/result/types.d.ts.map +1 -0
  12. package/dist/runtime/result/types.js +2 -0
  13. package/dist/runtime/result/types.js.map +1 -0
  14. package/dist/runtime/schema/core.d.ts +115 -0
  15. package/dist/runtime/schema/core.d.ts.map +1 -0
  16. package/dist/runtime/schema/core.js +259 -0
  17. package/dist/runtime/schema/core.js.map +1 -0
  18. package/dist/runtime/schema/shared.d.ts +54 -0
  19. package/dist/runtime/schema/shared.d.ts.map +1 -0
  20. package/dist/runtime/schema/shared.js +489 -0
  21. package/dist/runtime/schema/shared.js.map +1 -0
  22. package/dist/types.d.ts +7 -0
  23. package/dist/types.d.ts.map +1 -1
  24. package/include/mikrojs/cbor_helpers.h +20 -0
  25. package/include/mikrojs/mem.h +11 -0
  26. package/include/mikrojs/mikrojs.h +2 -1
  27. package/include/mikrojs/ota_client.h +342 -0
  28. package/include/mikrojs/ota_config.h +100 -0
  29. package/include/mikrojs/ota_env.h +192 -0
  30. package/include/mikrojs/ota_js_hooks.h +71 -0
  31. package/include/mikrojs/ota_policy.h +131 -0
  32. package/include/mikrojs/ota_slots.h +47 -0
  33. package/include/mikrojs/sys_codec.h +61 -0
  34. package/package.json +7 -5
  35. package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
  36. package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
  37. package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
  38. package/runtime/internal.d.ts +22 -16
  39. package/runtime/kv/shared.ts +11 -5
  40. package/runtime/kv/types.ts +4 -4
  41. package/runtime/ota/client.ts +12 -51
  42. package/runtime/ota/config.ts +18 -0
  43. package/runtime/ota/ota.ts +28 -70
  44. package/runtime/ota/types.ts +220 -2
  45. package/runtime/schema/core.ts +539 -0
  46. package/runtime/schema/schema.ts +36 -314
  47. package/runtime/schema/shared.ts +494 -0
  48. package/runtime/schema/types.ts +84 -12
  49. package/scripts/bundle-runtime.js +33 -0
  50. package/scripts/gen-checkin-fixtures.js +323 -0
  51. package/src/builtins.cpp +7 -8
  52. package/src/fs.cpp +3 -0
  53. package/src/mem.cpp +38 -0
  54. package/src/mik_abort.cpp +8 -1
  55. package/src/mik_cbor.cpp +43 -5
  56. package/src/mik_inspect.cpp +128 -22
  57. package/src/mik_ota_client.cpp +1230 -0
  58. package/src/mik_ota_config.cpp +296 -0
  59. package/src/mik_ota_js_hooks.cpp +190 -0
  60. package/src/mik_ota_policy.cpp +419 -0
  61. package/src/mik_ota_slots.cpp +249 -0
  62. package/src/mik_repl.cpp +9 -3
  63. package/src/mik_result.cpp +3 -1
  64. package/src/mik_sys_codec.cpp +167 -0
  65. package/src/mikrojs.cpp +15 -0
  66. package/src/modules.cpp +32 -13
  67. package/runtime/ota/client-impl.ts +0 -590
  68. package/runtime/ota/policy.ts +0 -299
@@ -86,7 +86,9 @@ JSValue mik__result_err(JSContext* ctx, int code, int platform_errno, const char
86
86
  JSValue mik__result_err_named(JSContext* ctx, const char* name, const char* fmt, ...) {
87
87
  va_list ap;
88
88
  va_start(ap, fmt);
89
- char msg[256];
89
+ /* 384 matches the HTTP task's error_buf: fetch errors pass through here
90
+ * verbatim, and a smaller buffer truncates their trailing heap figures. */
91
+ char msg[384];
90
92
  vsnprintf(msg, sizeof(msg), fmt, ap);
91
93
  va_end(ap);
92
94
 
@@ -0,0 +1,167 @@
1
+ #include "mikrojs/sys_codec.h"
2
+
3
+ #include <nanocbor/nanocbor.h>
4
+
5
+ #include <cstdint>
6
+ #include <cstdio>
7
+ #include <cstring>
8
+
9
+ size_t mik__kv_encode_str(const char* value, uint8_t* out, size_t out_len) {
10
+ /* The measuring pass needs a non-null base: an empty string still reaches
11
+ * memcpy with length 0, and memcpy(NULL, ..., 0) is undefined behavior. */
12
+ static uint8_t measure_base;
13
+ nanocbor_encoder_t enc;
14
+ nanocbor_encoder_init(&enc, &measure_base, 0);
15
+ nanocbor_put_tstr(&enc, value ? value : "");
16
+ size_t needed = nanocbor_encoded_len(&enc);
17
+ if (!out || out_len < needed) return needed;
18
+ nanocbor_encoder_init(&enc, out, out_len);
19
+ nanocbor_put_tstr(&enc, value ? value : "");
20
+ return needed;
21
+ }
22
+
23
+ bool mik__kv_decode_str(const uint8_t* in, size_t in_len, char* out, size_t out_len) {
24
+ if (!in || in_len == 0 || !out || out_len == 0) return false;
25
+ nanocbor_value_t it;
26
+ nanocbor_decoder_init(&it, in, in_len);
27
+ const uint8_t* ptr = nullptr;
28
+ size_t len = 0;
29
+ if (nanocbor_get_tstr(&it, &ptr, &len) < 0) return false;
30
+ if (len + 1 > out_len) return false;
31
+ memcpy(out, ptr, len);
32
+ out[len] = '\0';
33
+ return true;
34
+ }
35
+
36
+ size_t mik__kv_encode_i32(int32_t value, uint8_t* out, size_t out_len) {
37
+ static uint8_t measure_base;
38
+ nanocbor_encoder_t enc;
39
+ nanocbor_encoder_init(&enc, &measure_base, 0);
40
+ nanocbor_fmt_int(&enc, value);
41
+ size_t needed = nanocbor_encoded_len(&enc);
42
+ if (!out || out_len < needed) return needed;
43
+ nanocbor_encoder_init(&enc, out, out_len);
44
+ nanocbor_fmt_int(&enc, value);
45
+ return needed;
46
+ }
47
+
48
+ bool mik__kv_decode_i32(const uint8_t* in, size_t in_len, int32_t* out) {
49
+ if (!in || in_len == 0 || !out) return false;
50
+ nanocbor_value_t it;
51
+ nanocbor_decoder_init(&it, in, in_len);
52
+ return nanocbor_get_int32(&it, out) >= 0;
53
+ }
54
+
55
+
56
+ /* ── Device name pair ─────────────────────────────────────────────────────── */
57
+
58
+ namespace {
59
+
60
+ const char* skip_space(const char* p) {
61
+ while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
62
+ return p;
63
+ }
64
+
65
+ } // namespace
66
+
67
+ bool mik__device_name_parse(const char* json, int* out_rev, char* out_name, size_t name_len) {
68
+ if (!json || !out_rev || !out_name || name_len == 0) return false;
69
+ out_name[0] = '\0';
70
+
71
+ const char* p = skip_space(json);
72
+ if (*p != '[') return false;
73
+ p = skip_space(p + 1);
74
+
75
+ /* The revision. Negative and non-integer values are not pairs. */
76
+ if (*p < '0' || *p > '9') return false;
77
+ long rev = 0;
78
+ while (*p >= '0' && *p <= '9') {
79
+ rev = rev * 10 + (*p - '0');
80
+ if (rev > INT32_MAX) return false;
81
+ p++;
82
+ }
83
+ p = skip_space(p);
84
+ *out_rev = (int)rev;
85
+
86
+ if (*p == ']') return true; /* [rev] — cleared */
87
+ if (*p != ',') return false;
88
+ p = skip_space(p + 1);
89
+
90
+ /* A cleared name may also arrive as an explicit null. */
91
+ if (strncmp(p, "null", 4) == 0) {
92
+ p = skip_space(p + 4);
93
+ return *p == ']';
94
+ }
95
+ if (*p != '"') return false;
96
+ p++;
97
+
98
+ size_t written = 0;
99
+ while (*p && *p != '"') {
100
+ /* Escapes: the writer is JSON.stringify, so a name can carry them. Only
101
+ * the forms it emits are handled; anything else fails the parse rather
102
+ * than storing a half-decoded name. */
103
+ char c = *p;
104
+ if (c == '\\') {
105
+ p++;
106
+ switch (*p) {
107
+ case '"':
108
+ case '\\':
109
+ case '/':
110
+ c = *p;
111
+ break;
112
+ case 'n':
113
+ c = '\n';
114
+ break;
115
+ case 't':
116
+ c = '\t';
117
+ break;
118
+ case 'r':
119
+ c = '\r';
120
+ break;
121
+ case 'b':
122
+ c = '\b';
123
+ break;
124
+ case 'f':
125
+ c = '\f';
126
+ break;
127
+ default:
128
+ return false;
129
+ }
130
+ }
131
+ if (written + 1 >= name_len) return false;
132
+ out_name[written++] = c;
133
+ p++;
134
+ }
135
+ if (*p != '"') return false;
136
+ out_name[written] = '\0';
137
+ p = skip_space(p + 1);
138
+ return *p == ']';
139
+ }
140
+
141
+ size_t mik__device_name_format(int rev, const char* name, char* out, size_t out_len) {
142
+ if (rev < 0) rev = 0;
143
+ if (!name || !name[0]) {
144
+ int needed = snprintf(nullptr, 0, "[%d]", rev);
145
+ if (out && out_len > (size_t)needed) snprintf(out, out_len, "[%d]", rev);
146
+ return (size_t)needed;
147
+ }
148
+ /* Escape what JSON requires. Control characters below 0x20 have no short
149
+ * form here, so they are dropped rather than emitted raw — a name is display
150
+ * text, and a device is not the place to grow a \u encoder. */
151
+ char escaped[128];
152
+ size_t w = 0;
153
+ for (const char* p = name; *p && w + 2 < sizeof(escaped); p++) {
154
+ unsigned char c = (unsigned char)*p;
155
+ if (c == '"' || c == '\\') {
156
+ escaped[w++] = '\\';
157
+ escaped[w++] = (char)c;
158
+ } else if (c >= 0x20) {
159
+ escaped[w++] = (char)c;
160
+ }
161
+ }
162
+ escaped[w] = '\0';
163
+
164
+ int needed = snprintf(nullptr, 0, "[%d,\"%s\"]", rev, escaped);
165
+ if (out && out_len > (size_t)needed) snprintf(out, out_len, "[%d,\"%s\"]", rev, escaped);
166
+ return (size_t)needed;
167
+ }
package/src/mikrojs.cpp CHANGED
@@ -16,7 +16,16 @@
16
16
  #include "mikrojs/private.h"
17
17
  #include "mikrojs/utils.h"
18
18
 
19
+ #ifndef __has_feature
20
+ #define __has_feature(x) 0
21
+ #endif
22
+ #if defined(__SANITIZE_ADDRESS__) || __has_feature(address_sanitizer)
23
+ /* ASan inflates native frames severalfold; the regular default overflows
24
+ * on shallow JS recursion in instrumented builds. Never active on device. */
25
+ #define MIK__DEFAULT_STACK_SIZE 8 * 1024 * 1024
26
+ #else
19
27
  #define MIK__DEFAULT_STACK_SIZE 1024 * 1024 // 1 MB
28
+ #endif
20
29
 
21
30
  /* JS malloc functions. Routed through the mik__js_* family so the QuickJS
22
31
  * heap can be relocated to PSRAM on chips with both internal SRAM and PSRAM,
@@ -497,6 +506,12 @@ void MIK_RegisterNativeModuleInit(MIKRuntime* mik_rt, const char* name,
497
506
 
498
507
  void MIK_RegisterLoopConsumer(MIKRuntime* mik_rt, MIKLoopConsumeFn consume_fn,
499
508
  MIKLoopDestroyFn destroy_fn) {
509
+ /* Idempotent: a module can be brought up twice — once by native code that
510
+ * uses its machinery directly, once by the loader when JS imports it — and
511
+ * registering the pair twice would run its destroy twice at teardown. */
512
+ for (const auto& consumer : mik_rt->loop_consumers) {
513
+ if (consumer.consume_fn == consume_fn && consumer.destroy_fn == destroy_fn) return;
514
+ }
500
515
  mik_rt->loop_consumers.push_back({consume_fn, destroy_fn});
501
516
  }
502
517
 
package/src/modules.cpp CHANGED
@@ -61,10 +61,10 @@ static JSModuleDef* mik__load_bjs_module(JSContext* ctx, const char* module_name
61
61
  JS_FreeValue(ctx, func_val);
62
62
  return NULL;
63
63
  }
64
- if (JS_ResolveModule(ctx, func_val) < 0) {
65
- JS_FreeValue(ctx, func_val);
66
- return NULL;
67
- }
64
+ /* No JS_ResolveModule here: this runs inside the module loader, and its
65
+ * failure path frees every unresolved module in the context — including
66
+ * a partially-read parent an outer JS_ReadModule frame still holds. The
67
+ * root resolve covers loader-returned modules. */
68
68
  js_module_set_import_meta(ctx, func_val, true, false);
69
69
  JSModuleDef* m = static_cast<JSModuleDef*>(JS_VALUE_GET_PTR(func_val));
70
70
  JS_FreeValue(ctx, func_val);
@@ -452,7 +452,19 @@ static const char* mik__resolve_export_condition(JSContext* ctx, JSValue exp) {
452
452
  *
453
453
  * TODO: check publishConfig.exports before exports
454
454
  */
455
- static const char* mik__resolve_pkg_entry(JSContext* ctx, JSValue pkg_json, const char* subpath) {
455
+ /* Copy a JS_ToCString result into a js_malloc'd buffer, so every return
456
+ * from mik__resolve_pkg_entry uses one allocator — the "./index.js"
457
+ * fallback has no JSString to borrow, and mixing JS_FreeCString with
458
+ * js_strdup'd pointers walks back to a fake string header. */
459
+ static char* mik__dup_cstring(JSContext* ctx, const char* cstr) {
460
+ if (!cstr) return NULL;
461
+ char* copy = js_strdup(ctx, cstr);
462
+ JS_FreeCString(ctx, cstr);
463
+ return copy;
464
+ }
465
+
466
+ /* Returns a js_malloc'd entry path (caller frees with js_free), or NULL. */
467
+ static char* mik__resolve_pkg_entry(JSContext* ctx, JSValue pkg_json, const char* subpath) {
456
468
  JSValue exports = JS_GetPropertyStr(ctx, pkg_json, "exports");
457
469
 
458
470
  if (JS_IsUndefined(exports)) {
@@ -460,7 +472,7 @@ static const char* mik__resolve_pkg_entry(JSContext* ctx, JSValue pkg_json, cons
460
472
  /* No exports field — fall back to "main", then "./index.js" */
461
473
  JSValue main_val = JS_GetPropertyStr(ctx, pkg_json, "main");
462
474
  if (JS_IsString(main_val)) {
463
- const char* result = JS_ToCString(ctx, main_val);
475
+ char* result = mik__dup_cstring(ctx, JS_ToCString(ctx, main_val));
464
476
  JS_FreeValue(ctx, main_val);
465
477
  return result;
466
478
  }
@@ -470,7 +482,7 @@ static const char* mik__resolve_pkg_entry(JSContext* ctx, JSValue pkg_json, cons
470
482
 
471
483
  /* exports is a plain string — use it directly */
472
484
  if (JS_IsString(exports)) {
473
- const char* result = JS_ToCString(ctx, exports);
485
+ char* result = mik__dup_cstring(ctx, JS_ToCString(ctx, exports));
474
486
  JS_FreeValue(ctx, exports);
475
487
  return result;
476
488
  }
@@ -479,7 +491,7 @@ static const char* mik__resolve_pkg_entry(JSContext* ctx, JSValue pkg_json, cons
479
491
  if (JS_IsObject(exports)) {
480
492
  JSValue subpath_val = JS_GetPropertyStr(ctx, exports, subpath);
481
493
  if (!JS_IsUndefined(subpath_val) && !JS_IsException(subpath_val)) {
482
- const char* result = mik__resolve_export_condition(ctx, subpath_val);
494
+ char* result = mik__dup_cstring(ctx, mik__resolve_export_condition(ctx, subpath_val));
483
495
  JS_FreeValue(ctx, subpath_val);
484
496
  JS_FreeValue(ctx, exports);
485
497
  return result;
@@ -490,7 +502,7 @@ static const char* mik__resolve_pkg_entry(JSContext* ctx, JSValue pkg_json, cons
490
502
  * itself as a conditions object. This handles the common shorthand where
491
503
  * exports is {"default": "./index.js"} instead of {".": {"default": ...}} */
492
504
  if (strcmp(subpath, ".") == 0) {
493
- const char* result = mik__resolve_export_condition(ctx, exports);
505
+ char* result = mik__dup_cstring(ctx, mik__resolve_export_condition(ctx, exports));
494
506
  JS_FreeValue(ctx, exports);
495
507
  return result;
496
508
  }
@@ -593,7 +605,7 @@ static char* mik__try_resolve_pkg(JSContext* ctx, const char* dir, const MIKPkgS
593
605
  return NULL;
594
606
  }
595
607
 
596
- const char* entry = mik__resolve_pkg_entry(ctx, pkg_json, spec->subpath);
608
+ char* entry = mik__resolve_pkg_entry(ctx, pkg_json, spec->subpath);
597
609
  JS_FreeValue(ctx, pkg_json);
598
610
 
599
611
  if (!entry) {
@@ -608,7 +620,7 @@ static char* mik__try_resolve_pkg(JSContext* ctx, const char* dir, const MIKPkgS
608
620
  char result[PATH_MAX];
609
621
  snprintf(result, sizeof(result), "%s/node_modules/%.*s/%s", dir, spec->pkg_name_len,
610
622
  spec->pkg_name, entry_path);
611
- JS_FreeCString(ctx, entry);
623
+ js_free(ctx, entry);
612
624
  return js_strdup(ctx, result);
613
625
  }
614
626
 
@@ -754,6 +766,8 @@ static char* mik__module_normalizer_impl(JSContext* ctx, const char* base_name,
754
766
  } else {
755
767
  len = 0;
756
768
  }
769
+ /* A rooted base ('/main.js') has dirname '/', not "" */
770
+ bool rooted = base_name[0] == MIK__PATHSEP_POSIX;
757
771
 
758
772
  filename = static_cast<char*>(js_malloc(ctx, len + strlen(name) + 1 + 1));
759
773
  if (!filename) {
@@ -771,7 +785,12 @@ static char* mik__module_normalizer_impl(JSContext* ctx, const char* base_name,
771
785
  /* remove the last path element of filename, except if "."
772
786
  or ".." */
773
787
  if (filename[0] == '\0') {
774
- break;
788
+ if (!rooted) {
789
+ break;
790
+ }
791
+ /* the parent of the root is the root */
792
+ r += 3;
793
+ continue;
775
794
  }
776
795
  p = strrchr(filename, MIK__PATHSEP);
777
796
  if (!p) {
@@ -791,7 +810,7 @@ static char* mik__module_normalizer_impl(JSContext* ctx, const char* base_name,
791
810
  break;
792
811
  }
793
812
  }
794
- if (filename[0] != '\0') {
813
+ if (filename[0] != '\0' || rooted) {
795
814
  strcat(filename, MIK__PATHSEP_STR);
796
815
  }
797
816
  strcat(filename, r);