@mikrojs/native 0.8.0 → 0.10.0-next.7.gdacecb0

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 CHANGED
@@ -81,9 +81,9 @@ endif()
81
81
  include(cmake/mikrojs_bytecode.cmake)
82
82
  mikrojs_generate_bytecode(
83
83
  RUNTIME_DIR "${CMAKE_CURRENT_SOURCE_DIR}/runtime"
84
- MODULES cbor env result schema fs http/helpers http/request i2c kv/nvs kv/rtc kv/shared neopixel observable observable/lazy observable/operators pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi
85
- MODULE_PREFIX "mikrojs"
86
- SYMBOL_PREFIX "mikrojs"
84
+ MODULES cbor env result schema fs http/helpers http/request i2c kv/nvs kv/rtc kv/shared module neopixel observable observable/lazy observable/operators pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi
85
+ MODULE_PREFIX "mikro"
86
+ SYMBOL_PREFIX "mikro"
87
87
  TARGET gen_bytecode
88
88
  )
89
89
  add_dependencies(mikrojs gen_bytecode)
@@ -7,7 +7,7 @@
7
7
  # mikrojs_generate_bytecode(
8
8
  # RUNTIME_DIR <path> # Directory containing <mod>/<mod>.ts files
9
9
  # MODULES <mod1> <mod2> # Module names to compile
10
- # MODULE_PREFIX <prefix> # Module name prefix (e.g. "mikrojs" -> "mikrojs/pin")
10
+ # MODULE_PREFIX <prefix> # Module name prefix (e.g. "mikro" -> "mikro/pin")
11
11
  # SYMBOL_PREFIX <prefix> # C symbol prefix (e.g. "mikrojs" -> "mikrojs_pin_bytecode")
12
12
  # TARGET <name> # Custom target name (default: gen_bytecode)
13
13
  # WORKING_DIRECTORY <dir> # Working dir for esbuild (default: CMAKE_CURRENT_SOURCE_DIR)
@@ -29,8 +29,20 @@ typedef enum MIKLogFlush {
29
29
  MIK_LOG_FLUSH_LINE = 1,
30
30
  } MIKLogFlush;
31
31
 
32
+ /* What the device does after an uncaught exception (a "panic"). Mirror of
33
+ * the `onPanic.mode` field in the host-side TS config. */
34
+ typedef enum MIKPanicMode {
35
+ MIK_PANIC_RESTART = 0, /* default: reboot after the grace window */
36
+ MIK_PANIC_DEEP_SLEEP = 1, /* deep-sleep panic_sleep_duration_ms; wake reboots */
37
+ } MIKPanicMode;
38
+
32
39
  typedef struct MIKConfig {
40
+ /* Grace window after a panic, awake and reachable, before the action
41
+ * fires (applies to both modes). */
33
42
  int panic_restart_delay_ms;
43
+ MIKPanicMode panic_mode;
44
+ /* Deep-sleep length for MIK_PANIC_DEEP_SLEEP; ignored otherwise. */
45
+ int panic_sleep_duration_ms;
34
46
  size_t stack_size;
35
47
  uint32_t mem_reserved;
36
48
  uint32_t fs_read_max; /* 0 = keep runtime default (65536) */
@@ -14,6 +14,10 @@ typedef struct MIKPlatform {
14
14
  int64_t (*get_rtc_us)(void); /* RTC timer, survives deep sleep */
15
15
  uint32_t (*random)(void);
16
16
  void (*restart)(void);
17
+ /** Enter deep sleep for `us` microseconds; the timer wake reboots the
18
+ * chip, so this never returns on hardware. NULL on platforms without
19
+ * deep sleep (hosts), where callers fall back to restart(). */
20
+ void (*deep_sleep_us)(uint64_t us);
17
21
  void (*yield)(void);
18
22
  size_t (*get_free_system_mem)(void);
19
23
  size_t (*get_min_free_system_mem)(void); /* All-time low watermark */
@@ -184,6 +184,16 @@ char* mik_module_normalizer(JSContext* ctx, const char* base_name, const char* n
184
184
  * mid-evaluation, or unknown modules. */
185
185
  int mik__unload_module(JSContext* ctx, const char* normalized_name);
186
186
 
187
+ /* Unload the module whose namespace object is `ns` (reverse lookup via
188
+ * JS_FindModuleByNamespace, then mik__unload_module by name). Returns the
189
+ * number of modules freed, 0 if `ns` is not a currently-loaded module's
190
+ * namespace, or -1 with a pending JS exception. Backs `disposable()`. */
191
+ int mik__unload_namespace(JSContext* ctx, JSValueConst ns);
192
+
193
+ /* True if `ns` is the namespace of a currently-loaded, non-anchored module
194
+ * (i.e. one that disposable() is allowed to unload). */
195
+ bool mik__is_unloadable_namespace(JSContext* ctx, JSValueConst ns);
196
+
187
197
  int js_module_set_import_meta(JSContext* ctx, JSValue func_val, bool use_realpath, bool is_main);
188
198
 
189
199
  JSValue mik__get_args(JSContext* ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikrojs/native",
3
- "version": "0.8.0",
3
+ "version": "0.10.0-next.7.gdacecb0",
4
4
  "description": "Mikro.js C++ runtime library and Node.js native addon",
5
5
  "keywords": [
6
6
  "esp32",
@@ -9,15 +9,15 @@
9
9
  "quickjs",
10
10
  "runtime"
11
11
  ],
12
- "homepage": "https://github.com/mikrojs/mikrojs#readme",
12
+ "homepage": "https://github.com/mikrojs/mikro#readme",
13
13
  "bugs": {
14
- "url": "https://github.com/mikrojs/mikrojs/issues"
14
+ "url": "https://github.com/mikrojs/mikro/issues"
15
15
  },
16
16
  "license": "MIT",
17
17
  "author": "Bjørge Næss <bjoerge@gmail.com>",
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "git+https://github.com/mikrojs/mikrojs.git"
20
+ "url": "git+https://github.com/mikrojs/mikro.git"
21
21
  },
22
22
  "files": [
23
23
  "dist",
@@ -55,6 +55,7 @@
55
55
  "./runtime/inspect/types": "./runtime/inspect/types.ts",
56
56
  "./runtime/kv/shared": "./runtime/kv/shared.ts",
57
57
  "./runtime/kv/types": "./runtime/kv/types.ts",
58
+ "./runtime/module/types": "./runtime/module/types.ts",
58
59
  "./runtime/neopixel/types": "./runtime/neopixel/types.ts",
59
60
  "./runtime/observable/operators": "./runtime/observable/operators.ts",
60
61
  "./runtime/observable/types": "./runtime/observable/types.ts",
@@ -78,7 +79,7 @@
78
79
  "cmake-js": "^8.0.0",
79
80
  "node-addon-api": "^8.7.0",
80
81
  "node-gyp-build": "^4.8.4",
81
- "@mikrojs/quickjs": "0.8.0"
82
+ "@mikrojs/quickjs": "0.10.0-next.7.gdacecb0"
82
83
  },
83
84
  "devDependencies": {
84
85
  "@swc/core": "^1.15.30",
@@ -1,5 +1,5 @@
1
- import {lazyEvent} from 'mikrojs/observable/lazy'
2
- import {err, ok} from 'mikrojs/result'
1
+ import {lazyEvent} from 'mikro/observable/lazy'
2
+ import {err, ok} from 'mikro/result'
3
3
  import {Ble as NativeBle} from 'native:ble'
4
4
 
5
5
  import type {Result} from '../result/types.js'
package/runtime/fs/fs.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {err, ok} from 'mikrojs/result'
1
+ import {err, ok} from 'mikro/result'
2
2
  import * as native from 'native:fs'
3
3
 
4
4
  import type {Result} from '../result/types.js'
@@ -64,28 +64,4 @@ declare interface ImportMeta {
64
64
  readonly basename: string
65
65
  readonly path: string
66
66
  readonly env: Readonly<Record<string, string | undefined>>
67
- /**
68
- * Evict a module from the runtime's module cache, recursively freeing
69
- * any transitive dependency whose only importer was the unloaded module.
70
- * Builtin modules (native:*, mikrojs/*, @mikrojs/*) are anchored
71
- * and cannot be unloaded.
72
- *
73
- * Intended for use with dynamic imports: a static `import` at the top of
74
- * the module creates a binding that pins the imported module's exports,
75
- * so unloading it while the binding is live reclaims nothing. Prefer
76
- * `const x = await import(spec)` + local scope + `import.meta.unload(spec)`.
77
- *
78
- * **Incompatible with `bundle: true`.** When the build bundles modules
79
- * into chunks, source-level specifiers like `./phases/display.js` no
80
- * longer correspond to loaded module names (they become chunk names like
81
- * `display-GYVGL5MB.js`) — the unload call silently resolves to 0 and
82
- * frees nothing. Projects that rely on dynamic-import-then-unload for
83
- * memory reclaim need `bundle: false` so specifier strings survive to
84
- * runtime unchanged.
85
- *
86
- * Resolves to the number of modules actually freed (root + transitive
87
- * orphans), after a GC pass has run so a subsequent `memoryUsage()`
88
- * snapshot reflects the reclaim.
89
- */
90
- unload(specifier: string): Promise<number>
91
67
  }
@@ -3,7 +3,7 @@
3
3
  // LTE modem over UART) implement the `Request` type directly and reuse
4
4
  // `prepareBody` + `makeResponse` for the boring parts.
5
5
 
6
- import {err, ok} from 'mikrojs/result'
6
+ import {err, ok} from 'mikro/result'
7
7
 
8
8
  import type {Result} from '../result/types.js'
9
9
 
@@ -8,8 +8,8 @@ import {
8
8
  type Request,
9
9
  RequestError,
10
10
  type RequestOptions,
11
- } from 'mikrojs/http/helpers'
12
- import {err, ok} from 'mikrojs/result'
11
+ } from 'mikro/http/helpers'
12
+ import {err, ok} from 'mikro/result'
13
13
 
14
14
  import type {ErrResult, Result} from '../result/types.js'
15
15
 
@@ -4,20 +4,20 @@ type NR<T> = {ok: true; value: T} | {ok: false; error: {code: number; message: s
4
4
 
5
5
  // Internal shared runtime modules. Registered as builtins so sibling bundles
6
6
  // can reference them at runtime to dedupe shared code, but not exposed via
7
- // the public `mikrojs/*` subpath exports so user apps can't accidentally
7
+ // the public `mikro/*` subpath exports so user apps can't accidentally
8
8
  // import them. Keep these in sync with the MODULES list in the firmware +
9
9
  // @mikrojs/native CMakeLists.
10
- declare module 'mikrojs/kv/shared' {
10
+ declare module 'mikro/kv/shared' {
11
11
  export {KVError, makeCreateValue, type NativeKvFns} from './kv/shared.js'
12
12
  }
13
13
 
14
- declare module 'mikrojs/observable/lazy' {
14
+ declare module 'mikro/observable/lazy' {
15
15
  export {lazyEvent} from './observable/lazy.js'
16
16
  }
17
17
 
18
18
  declare module 'native:cbor' {
19
19
  import type {CborError} from '@mikrojs/native/runtime/cbor/types'
20
- import type {Result} from 'mikrojs/result'
20
+ import type {Result} from 'mikro/result'
21
21
  export function encode(value: unknown): Result<Uint8Array, CborError>
22
22
  export function decode(data: Uint8Array): Result<unknown, CborError>
23
23
  }
@@ -46,6 +46,11 @@ declare module 'native:sys' {
46
46
  }
47
47
  export function jsMemoryUsage(): JsMemoryUsage
48
48
  export function gc(): void
49
+ /** Unload the module whose namespace object is `ns`; returns the number of
50
+ * modules freed (0 if `ns` is not a loaded module's namespace). */
51
+ export function unloadNamespace(ns: object): number
52
+ /** True if `ns` is the namespace of a loaded, non-anchored (non-builtin) module. */
53
+ export function isUnloadableNamespace(ns: object): boolean
49
54
  export function activeTimers(): number
50
55
  export function setTime(millisSinceEpoch: number): void
51
56
  export function uptime(): {boot: number; rtc: number}
@@ -117,7 +122,7 @@ declare module 'native:stdio' {
117
122
 
118
123
  declare module 'native:pin' {
119
124
  import type {PinError} from '@mikrojs/native/runtime/pin/types'
120
- import type {Result} from 'mikrojs/result'
125
+ import type {Result} from 'mikro/result'
121
126
  export type PinMode = 0x01 | 0x03 | 0x05
122
127
  export function pinMode(pin: number, value: PinMode): Result<void, PinError>
123
128
  export function digitalWrite(pin: number, value: 0 | 1): Result<void, PinError>
@@ -138,8 +143,8 @@ declare module 'native:sleep' {
138
143
  }
139
144
 
140
145
  declare module 'native:http' {
141
- import type {RequestError} from 'mikrojs/http/helpers'
142
- import type {Result} from 'mikrojs/result'
146
+ import type {RequestError} from 'mikro/http/helpers'
147
+ import type {Result} from 'mikro/result'
143
148
 
144
149
  import type {ErrResult} from './result/types.js'
145
150
  type HeadersMsg = {status: number; headers: [string, string][]}
@@ -160,7 +165,7 @@ declare module 'native:http' {
160
165
  }
161
166
  declare module 'native:i2c' {
162
167
  import type {I2cError} from '@mikrojs/native/runtime/i2c/types'
163
- import type {Result} from 'mikrojs/result'
168
+ import type {Result} from 'mikro/result'
164
169
 
165
170
  export interface I2cBaseOptions {
166
171
  freq?: number
@@ -192,7 +197,7 @@ declare module 'native:i2c' {
192
197
  }
193
198
  declare module 'native:spi' {
194
199
  import type {SpiError} from '@mikrojs/native/runtime/spi/types'
195
- import type {Result} from 'mikrojs/result'
200
+ import type {Result} from 'mikro/result'
196
201
 
197
202
  export interface SpiOptions {
198
203
  clk: number
@@ -221,7 +226,7 @@ declare module 'native:spi' {
221
226
 
222
227
  declare module 'native:sntp' {
223
228
  import type {SntpError} from '@mikrojs/native/runtime/sntp/types'
224
- import type {Result} from 'mikrojs/result'
229
+ import type {Result} from 'mikro/result'
225
230
  /**
226
231
  * Native errors: the outer Result covers init/config failures (InitFailed);
227
232
  * the inner Result on resolve covers post-start errors (Cancelled).
@@ -257,7 +262,7 @@ declare module 'native:nvs_kv' {
257
262
 
258
263
  declare module 'native:pwm' {
259
264
  import type {PwmError} from '@mikrojs/native/runtime/pwm/types'
260
- import type {Result} from 'mikrojs/result'
265
+ import type {Result} from 'mikro/result'
261
266
 
262
267
  export declare const Pwm: {
263
268
  prototype: Pwm
@@ -274,7 +279,7 @@ declare module 'native:pwm' {
274
279
 
275
280
  declare module 'native:neopixel' {
276
281
  import type {NeoPixelError} from '@mikrojs/native/runtime/neopixel/types'
277
- import type {Result} from 'mikrojs/result'
282
+ import type {Result} from 'mikro/result'
278
283
 
279
284
  export declare const NeoPixel: {
280
285
  prototype: NeoPixel
@@ -292,7 +297,7 @@ declare module 'native:neopixel' {
292
297
 
293
298
  declare module 'native:uart' {
294
299
  import type {UartError} from '@mikrojs/native/runtime/uart/types'
295
- import type {Result} from 'mikrojs/result'
300
+ import type {Result} from 'mikro/result'
296
301
 
297
302
  export interface UartOptions {
298
303
  tx?: number
@@ -324,7 +329,7 @@ declare module 'native:uart' {
324
329
 
325
330
  declare module 'native:ble' {
326
331
  import type {BleError} from '@mikrojs/native/runtime/ble/types'
327
- import type {Result} from 'mikrojs/result'
332
+ import type {Result} from 'mikro/result'
328
333
 
329
334
  type R<T> = Result<T, BleError>
330
335
 
@@ -378,7 +383,7 @@ declare module 'native:ble' {
378
383
 
379
384
  declare module 'native:wifi' {
380
385
  import type {WifiError} from '@mikrojs/native/runtime/wifi/types'
381
- import type {Result} from 'mikrojs/result'
386
+ import type {Result} from 'mikro/result'
382
387
 
383
388
  type R<T> = Result<T, WifiError>
384
389
 
@@ -464,7 +469,7 @@ declare module 'native:udp' {
464
469
  UdpError,
465
470
  UdpFamily,
466
471
  } from '@mikrojs/native/runtime/udp/types'
467
- import type {Result} from 'mikrojs/result'
472
+ import type {Result} from 'mikro/result'
468
473
 
469
474
  export interface NativeUdpSocket {
470
475
  readonly port: number
package/runtime/kv/nvs.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {KVError, makeCreateValue} from 'mikrojs/kv/shared'
1
+ import {KVError, makeCreateValue} from 'mikro/kv/shared'
2
2
  import {clear, get, info, remove, set} from 'native:nvs_kv'
3
3
 
4
4
  import type {NvsStorage} from './types.js'
package/runtime/kv/rtc.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {KVError, makeCreateValue} from 'mikrojs/kv/shared'
1
+ import {KVError, makeCreateValue} from 'mikro/kv/shared'
2
2
  import {clear, get, info, remove, set} from 'native:rtc'
3
3
 
4
4
  import type {RtcStorage} from './types.js'
@@ -1,7 +1,7 @@
1
- import {err, ok} from 'mikrojs/result'
1
+ import {err, ok} from 'mikro/result'
2
2
  // Typed loosely to avoid deep type instantiation from Infer<S> in parse's generics.
3
3
  // Type safety is provided by the storage interface overloads in types.ts.
4
- import {parse as _parse} from 'mikrojs/schema'
4
+ import {parse as _parse} from 'mikro/schema'
5
5
 
6
6
  import type {NativeError} from '../result/types.js'
7
7
 
@@ -0,0 +1,37 @@
1
+ import {isUnloadableNamespace, unloadNamespace} from 'native:sys'
2
+
3
+ /**
4
+ * Load a dynamically imported module, run `use` with it, then unload it and
5
+ * reclaim its memory. Use for app phases that pull in code you won't need again:
6
+ *
7
+ * ```ts
8
+ * const status = await withUnload(
9
+ * import('./phases/modem.js'),
10
+ * (modem) => modem.runPhase(input),
11
+ * )
12
+ * // the modem phase is unloaded and reclaimed by the time this resolves
13
+ * ```
14
+ *
15
+ * The module is unloaded when `use` returns (or throws), including any transitive
16
+ * dependency whose only importer was this module. Don't return one of the
17
+ * module's exports from `use`; that keeps the module alive. Throws if the
18
+ * resolved value isn't an unloadable module namespace (e.g. a builtin
19
+ * `mikrojs/*` or `native:*` module).
20
+ *
21
+ * This is non-standard: standard JavaScript modules stay loaded for the life of
22
+ * the program, and a later `import` re-evaluates an unloaded module from scratch.
23
+ */
24
+ export async function withUnload<T extends object, R>(
25
+ mod: Promise<T>,
26
+ use: (mod: T) => R | Promise<R>,
27
+ ): Promise<R> {
28
+ const ns = await mod
29
+ if (!isUnloadableNamespace(ns)) {
30
+ throw new TypeError('withUnload() expects an unloadable module namespace')
31
+ }
32
+ try {
33
+ return await use(ns)
34
+ } finally {
35
+ unloadNamespace(ns)
36
+ }
37
+ }
@@ -0,0 +1,21 @@
1
+ /** Load a dynamically imported module, run `use` with it, then unload it and
2
+ * reclaim its memory:
3
+ *
4
+ * ```ts
5
+ * const status = await withUnload(
6
+ * import('./phases/modem.js'),
7
+ * (modem) => modem.runPhase(input),
8
+ * )
9
+ * ```
10
+ *
11
+ * The module (and any transitive dependency whose only importer was it) is
12
+ * unloaded when `use` returns or throws. Don't return one of the module's
13
+ * exports from `use`; that keeps it alive. Throws if the resolved value isn't
14
+ * an unloadable module namespace (e.g. a builtin `mikrojs/*` or `native:*`).
15
+ *
16
+ * Non-standard: standard JavaScript modules stay loaded for the life of the
17
+ * program, and a later `import` re-evaluates an unloaded module from scratch. */
18
+ export declare function withUnload<T extends object, R>(
19
+ mod: Promise<T>,
20
+ use: (mod: T) => R | Promise<R>,
21
+ ): Promise<R>
@@ -1,4 +1,4 @@
1
- import {ok} from 'mikrojs/result'
1
+ import {ok} from 'mikro/result'
2
2
  import {Pwm as NativePwm} from 'native:pwm'
3
3
 
4
4
  import type {Result} from '../result/types.js'
@@ -9,7 +9,7 @@
9
9
  * lazy-load efficiency.
10
10
  */
11
11
 
12
- import {err, ok} from 'mikrojs/result'
12
+ import {err, ok} from 'mikro/result'
13
13
 
14
14
  import type {Result} from '../result/types.js'
15
15
 
@@ -7,7 +7,7 @@
7
7
  * `packages/mikrojs` and by user apps via their tsconfig.
8
8
  */
9
9
 
10
- import type {Result} from 'mikrojs/result'
10
+ import type {Result} from 'mikro/result'
11
11
 
12
12
  export type ReaderError = {name: 'Timeout'; ms: number} | {name: 'StreamClosed'}
13
13
 
@@ -1,4 +1,4 @@
1
- import {err, ok} from 'mikrojs/result'
1
+ import {err, ok} from 'mikro/result'
2
2
 
3
3
  import type {Result} from '../result/types.js'
4
4
 
@@ -1,4 +1,4 @@
1
- import {err, ok} from 'mikrojs/result'
1
+ import {err, ok} from 'mikro/result'
2
2
  import {setTimezone, stop as nativeStop, sync as nativeSync} from 'native:sntp'
3
3
 
4
4
  import type {Result} from '../result/types.js'
@@ -14,7 +14,7 @@
14
14
  * Expand only when a second call site asks for it.
15
15
  */
16
16
 
17
- import {err, ok} from 'mikrojs/result'
17
+ import {err, ok} from 'mikro/result'
18
18
 
19
19
  import type {Result} from '../result/types.js'
20
20
 
@@ -11,7 +11,7 @@
11
11
  * errors widen the error type with a `StreamError` variant.
12
12
  */
13
13
 
14
- import type {Result} from 'mikrojs/result'
14
+ import type {Result} from 'mikro/result'
15
15
 
16
16
  export type StreamError = {name: 'Timeout'; ms: number} | {name: 'StreamClosed'}
17
17
 
@@ -1,4 +1,4 @@
1
- import {err, ok, PanicError, type Result} from 'mikrojs/result'
1
+ import {err, ok, PanicError, type Result} from 'mikro/result'
2
2
  import {getWakeupCause as nativeGetWakeupCause} from 'native:sleep'
3
3
  import * as native from 'native:sys'
4
4
 
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable no-console */
2
- import {gc, memoryUsage, MonotonicTimestamp} from 'mikrojs/sys'
2
+ import {gc, memoryUsage, MonotonicTimestamp} from 'mikro/sys'
3
3
  import {pendingCount as pendingHttpCount} from 'native:http'
4
4
  import {activeTimers} from 'native:sys'
5
5
 
@@ -1,4 +1,4 @@
1
- import {ok} from 'mikrojs/result'
1
+ import {ok} from 'mikro/result'
2
2
  import * as native from 'native:uart'
3
3
 
4
4
  import type {Result} from '../result/types.js'
@@ -1,4 +1,4 @@
1
- import {ok} from 'mikrojs/result'
1
+ import {ok} from 'mikro/result'
2
2
  import {Observable} from 'native:observable'
3
3
  import {bind as nativeBind, type NativeUdpSocket} from 'native:udp'
4
4
 
@@ -1,5 +1,5 @@
1
- import {lazyEvent} from 'mikrojs/observable/lazy'
2
- import {err, ok} from 'mikrojs/result'
1
+ import {lazyEvent} from 'mikro/observable/lazy'
2
+ import {err, ok} from 'mikro/result'
3
3
  import {Wifi as NativeWifi} from 'native:wifi'
4
4
 
5
5
  import type {Result} from '../result/types.js'
@@ -104,7 +104,7 @@ for (const name of moduleNames) {
104
104
  target: 'es2024',
105
105
  platform: 'neutral',
106
106
  format: 'esm',
107
- external: ['mikrojs', 'mikrojs/*', '@mikrojs/*', 'native:*'],
107
+ external: ['mikro', 'mikro/*', '@mikrojs/*', 'native:*'],
108
108
  })
109
109
 
110
110
  const output = result.outputFiles?.[0]
@@ -128,7 +128,7 @@ for (const name of moduleNames) {
128
128
  // Extract external imports so CMake can pass them as -M flags to qjsc
129
129
  const externals = []
130
130
  for (const match of source.matchAll(
131
- /(?:from|import)\s*["']((?:native:|mikrojs\/|@mikrojs\/)[^"']+)["']/g,
131
+ /(?:from|import)\s*["']((?:native:|mikro\/|@mikrojs\/)[^"']+)["']/g,
132
132
  )) {
133
133
  externals.push(match[1])
134
134
  }
package/src/builtins.cpp CHANGED
@@ -14,8 +14,8 @@ typedef struct {
14
14
 
15
15
  /* Generated by mikrojs_generate_bytecode() in CMake — includes + lookup table.
16
16
  * Adding a new module only requires updating the MODULES list in CMakeLists.txt. */
17
- #include "gen/mikrojs_builtins_table.h"
18
- #define builtins mikrojs_builtins
17
+ #include "gen/mikro_builtins_table.h"
18
+ #define builtins mikro_builtins
19
19
 
20
20
  /* External builtins registered by board/driver packages via MIK_REGISTER_BUILTIN() */
21
21
  // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
@@ -13,6 +13,8 @@ static const char* TAG = "mik_app_config";
13
13
 
14
14
  void MIK_DefaultConfig(MIKConfig* config) {
15
15
  config->panic_restart_delay_ms = 1000;
16
+ config->panic_mode = MIK_PANIC_RESTART;
17
+ config->panic_sleep_duration_ms = 0;
16
18
  config->stack_size = 0;
17
19
  config->mem_reserved = 64 * 1024;
18
20
  config->fs_read_max = 0; /* 0 = runtime default (65536) */
@@ -318,9 +320,19 @@ int MIK_LoadConfig(const char* base_path, MIKConfig* config) {
318
320
  if (buf) {
319
321
  double num_val;
320
322
 
321
- if (mik__json_get_number(buf, "panicRestartDelay", &num_val)) {
323
+ if (mik__json_get_number(buf, "onPanic.delay", &num_val)) {
322
324
  config->panic_restart_delay_ms = (int)num_val;
323
325
  }
326
+ char panic_mode_str[16];
327
+ if (mik__json_get_string(buf, "onPanic.mode", panic_mode_str,
328
+ sizeof(panic_mode_str))) {
329
+ config->panic_mode = strcmp(panic_mode_str, "deepSleep") == 0
330
+ ? MIK_PANIC_DEEP_SLEEP
331
+ : MIK_PANIC_RESTART;
332
+ }
333
+ if (mik__json_get_number(buf, "onPanic.duration", &num_val)) {
334
+ config->panic_sleep_duration_ms = (int)num_val;
335
+ }
324
336
  if (mik__json_get_number(buf, "stackSize", &num_val)) {
325
337
  config->stack_size = (size_t)num_val;
326
338
  }
package/src/mik_sys.cpp CHANGED
@@ -96,6 +96,24 @@ static JSValue mik__sys_gc(JSContext* ctx, JSValue this_val, int argc, JSValue*
96
96
  return JS_UNDEFINED;
97
97
  }
98
98
 
99
+ /* Backs withUnload()'s unload step. Unloads the module whose
100
+ * namespace is argv[0]; returns the number of modules freed (0 if not a
101
+ * loaded module's namespace). Refcounting reclaims the memory once the
102
+ * caller's reference drops; no GC here. */
103
+ static JSValue mik__sys_unload_namespace(JSContext* ctx, JSValue this_val, int argc,
104
+ JSValue* argv) {
105
+ int rv = mik__unload_namespace(ctx, argv[0]);
106
+ if (rv < 0) return JS_EXCEPTION;
107
+ return JS_NewInt32(ctx, rv);
108
+ }
109
+
110
+ /* True if argv[0] is the namespace of a loaded, non-anchored module.
111
+ * withUnload() uses this to reject builtins (native:/mikrojs//@mikrojs/). */
112
+ static JSValue mik__sys_is_unloadable_namespace(JSContext* ctx, JSValue this_val, int argc,
113
+ JSValue* argv) {
114
+ return JS_NewBool(ctx, mik__is_unloadable_namespace(ctx, argv[0]));
115
+ }
116
+
99
117
  static JSValue mik__sys_active_timers(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
100
118
  MIKRuntime* mik_rt = static_cast<MIKRuntime*>(JS_GetContextOpaque(ctx));
101
119
  CHECK_NOT_NULL(mik_rt);
@@ -220,6 +238,11 @@ void mik__sys_api_init(JSContext* ctx, JSValue ns) {
220
238
  JS_SetPropertyStr(ctx, ns, "jsMemoryUsage",
221
239
  JS_NewCFunction(ctx, mik__sys_js_memory_usage, "jsMemoryUsage", 0));
222
240
  JS_SetPropertyStr(ctx, ns, "gc", JS_NewCFunction(ctx, mik__sys_gc, "gc", 0));
241
+ JS_SetPropertyStr(ctx, ns, "unloadNamespace",
242
+ JS_NewCFunction(ctx, mik__sys_unload_namespace, "unloadNamespace", 1));
243
+ JS_SetPropertyStr(
244
+ ctx, ns, "isUnloadableNamespace",
245
+ JS_NewCFunction(ctx, mik__sys_is_unloadable_namespace, "isUnloadableNamespace", 1));
223
246
  JS_SetPropertyStr(ctx, ns, "activeTimers",
224
247
  JS_NewCFunction(ctx, mik__sys_active_timers, "activeTimers", 0));
225
248
  JS_SetPropertyStr(ctx, ns, "setTime",
@@ -242,44 +242,138 @@ static const JSCFunctionListEntry mik__text_decoder_proto_funcs[] = {
242
242
 
243
243
  static const char b64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
244
244
 
245
- static const uint8_t b64_decode_table[256] = {
245
+ /* The base64 decoder below is ported from QuickJS-NG (quickjs.c, `b64_decode`
246
+ * and its lookup tables), which implements the WHATWG forgiving-base64 decode
247
+ * algorithm. QuickJS-NG is MIT licensed:
248
+ * Copyright (c) 2017-2024 Fabrice Bellard, Charlie Gordon and contributors.
249
+ * We use only the standard alphabet (atob/btoa never decode base64url) and
250
+ * surface decode failures as our own error type rather than a DOMException. */
251
+ enum { K_VAL = 1u, K_WS = 2u };
252
+
253
+ /* Sextet value per base64 character (0 for non-base64 bytes; validity is gated
254
+ * by b64_flags below). Positional initializer to stay standard C++. */
255
+ static const uint8_t b64_val[256] = {
246
256
  /* clang-format off */
247
- 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
248
- 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
249
- 255,255,255,255,255,255,255,255,255,255,255, 62,255,255,255, 63,
250
- 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,255,255,255, 0,255,255,
251
- 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
252
- 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,255,255,255,255,255,
253
- 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
254
- 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,255,255,255,255,255,
255
- 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
256
- 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
257
- 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
258
- 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
259
- 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
260
- 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
261
- 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
262
- 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
257
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
258
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
259
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,62, 0, 0, 0,63,
260
+ 52,53,54,55,56,57,58,59,60,61, 0, 0, 0, 0, 0, 0,
261
+ 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
262
+ 15,16,17,18,19,20,21,22,23,24,25, 0, 0, 0, 0, 0,
263
+ 0,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
264
+ 41,42,43,44,45,46,47,48,49,50,51, 0, 0, 0, 0, 0,
265
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
266
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
267
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
268
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
269
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
270
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
271
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
272
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
263
273
  /* clang-format on */
264
274
  };
265
275
 
276
+ /* Per-byte class: K_WS (2) for ASCII whitespace, K_VAL (1) for the 64 standard
277
+ * base64 alphabet characters, 0 (invalid) otherwise. */
278
+ static const char b64_flags[256] = {
279
+ /* clang-format off */
280
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 2, 2, 0, 0,
281
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
282
+ 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1,
283
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
284
+ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
285
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
286
+ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
287
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
288
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
289
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
290
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
291
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
292
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
293
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
294
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
295
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
296
+ /* clang-format on */
297
+ };
298
+
299
+ /* Implements https://infra.spec.whatwg.org/#forgiving-base64-decode.
300
+ * Writes decoded bytes to dst and returns the count; sets *err on malformed
301
+ * input. dst must have room for at least (len / 4) * 3 + 3 bytes. */
302
+ static size_t b64_decode(const char* src, size_t len, uint8_t* dst, int* err) {
303
+ size_t i, j;
304
+ uint32_t acc;
305
+ int seen, pad;
306
+ unsigned ch;
307
+
308
+ acc = 0;
309
+ seen = 0;
310
+ for (i = 0, j = 0; i < len; i++) {
311
+ ch = (unsigned char)src[i];
312
+ if ((b64_flags[ch] & K_WS)) continue;
313
+ if (!(b64_flags[ch] & K_VAL)) break;
314
+ acc = (acc << 6) | b64_val[ch];
315
+ seen++;
316
+ if (seen == 4) {
317
+ dst[j++] = (acc >> 16) & 0xFF;
318
+ dst[j++] = (acc >> 8) & 0xFF;
319
+ dst[j++] = acc & 0xFF;
320
+ seen = 0;
321
+ acc = 0;
322
+ }
323
+ }
324
+
325
+ if (seen != 0) {
326
+ if (seen == 3) {
327
+ dst[j++] = (acc >> 10) & 0xFF;
328
+ dst[j++] = (acc >> 2) & 0xFF;
329
+ } else if (seen == 2) {
330
+ dst[j++] = (acc >> 4) & 0xFF;
331
+ } else {
332
+ *err = 1;
333
+ return 0;
334
+ }
335
+ for (pad = 0; i < len; i++) {
336
+ ch = (unsigned char)src[i];
337
+ if (pad < 2 && ch == '=') {
338
+ pad++;
339
+ } else if (!(b64_flags[ch] & K_WS)) {
340
+ break;
341
+ }
342
+ }
343
+ if (pad != 0 && seen + pad != 4) {
344
+ *err = 1;
345
+ return 0;
346
+ }
347
+ }
348
+
349
+ *err = i < len;
350
+ return j;
351
+ }
352
+
266
353
  /* btoa: encode a binary string to base64.
267
- * Per the web spec, each JS character's code point is treated as a raw byte.
268
- * We must iterate by code point, not by UTF-8 bytes, since JS_ToCStringLen
269
- * returns UTF-8 where e.g. U+0080 becomes two bytes (\xC2\x80). */
354
+ * Per the web spec, the argument is first coerced to a string (ToString), then
355
+ * each character's code point is treated as a raw byte. We iterate by code
356
+ * point, not by UTF-8 bytes, since JS_ToCStringLen would turn e.g. U+0080 into
357
+ * two bytes (\xC2\x80). */
270
358
  static JSValue mik__btoa(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
271
- JSValue str_val = argv[0];
359
+ JSValue str_val = JS_ToString(ctx, argv[0]);
360
+ if (JS_IsException(str_val)) return JS_EXCEPTION;
361
+
272
362
  JSValue len_val = JS_GetPropertyStr(ctx, str_val, "length");
273
363
  int64_t len;
274
364
  if (JS_ToInt64(ctx, &len, len_val)) {
275
365
  JS_FreeValue(ctx, len_val);
366
+ JS_FreeValue(ctx, str_val);
276
367
  return JS_EXCEPTION;
277
368
  }
278
369
  JS_FreeValue(ctx, len_val);
279
370
 
280
371
  /* Extract code points into a byte buffer, validating latin1 range */
281
372
  uint8_t* bytes = static_cast<uint8_t*>(js_malloc(ctx, len > 0 ? len : 1));
282
- if (!bytes) return JS_EXCEPTION;
373
+ if (!bytes) {
374
+ JS_FreeValue(ctx, str_val);
375
+ return JS_EXCEPTION;
376
+ }
283
377
 
284
378
  JSAtom charCodeAt_atom = JS_NewAtom(ctx, "charCodeAt");
285
379
  for (int64_t i = 0; i < len; i++) {
@@ -289,6 +383,7 @@ static JSValue mik__btoa(JSContext* ctx, JSValue this_val, int argc, JSValue* ar
289
383
  if (JS_IsException(code)) {
290
384
  JS_FreeAtom(ctx, charCodeAt_atom);
291
385
  js_free(ctx, bytes);
386
+ JS_FreeValue(ctx, str_val);
292
387
  return JS_EXCEPTION;
293
388
  }
294
389
  int32_t cp;
@@ -297,6 +392,7 @@ static JSValue mik__btoa(JSContext* ctx, JSValue this_val, int argc, JSValue* ar
297
392
  if (cp > 0xFF) {
298
393
  JS_FreeAtom(ctx, charCodeAt_atom);
299
394
  js_free(ctx, bytes);
395
+ JS_FreeValue(ctx, str_val);
300
396
  return JS_ThrowRangeError(ctx,
301
397
  "The string to be encoded contains characters outside of the "
302
398
  "Latin1 range");
@@ -304,6 +400,7 @@ static JSValue mik__btoa(JSContext* ctx, JSValue this_val, int argc, JSValue* ar
304
400
  bytes[i] = (uint8_t)cp;
305
401
  }
306
402
  JS_FreeAtom(ctx, charCodeAt_atom);
403
+ JS_FreeValue(ctx, str_val);
307
404
 
308
405
  size_t out_len = 4 * ((len + 2) / 3);
309
406
  char* out = static_cast<char*>(js_malloc(ctx, out_len + 1));
@@ -332,55 +429,32 @@ static JSValue mik__btoa(JSContext* ctx, JSValue this_val, int argc, JSValue* ar
332
429
  return ret;
333
430
  }
334
431
 
335
- /* atob: decode a base64 string to a binary string */
432
+ /* atob: decode a base64 string to a binary string. The argument is coerced to
433
+ * a string (ToString) per spec, then decoded via the ported forgiving-base64
434
+ * decoder above. */
336
435
  static JSValue mik__atob(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
436
+ JSValue str_val = JS_ToString(ctx, argv[0]);
437
+ if (JS_IsException(str_val)) return JS_EXCEPTION;
337
438
  size_t len;
338
- const char* str = JS_ToCStringLen(ctx, &len, argv[0]);
439
+ const char* str = JS_ToCStringLen(ctx, &len, str_val);
440
+ JS_FreeValue(ctx, str_val);
339
441
  if (!str) return JS_EXCEPTION;
340
442
 
341
- /* Strip whitespace and validate */
342
- size_t out_cap = (len * 3) / 4 + 1;
343
- char* out = static_cast<char*>(js_malloc(ctx, out_cap));
443
+ size_t out_cap = (len / 4) * 3 + 3;
444
+ uint8_t* out = static_cast<uint8_t*>(js_malloc(ctx, out_cap));
344
445
  if (!out) {
345
446
  JS_FreeCString(ctx, str);
346
447
  return JS_EXCEPTION;
347
448
  }
348
449
 
349
- uint32_t accum = 0;
350
- int bits = 0;
351
- size_t j = 0;
352
- int pad = 0;
353
-
354
- for (size_t i = 0; i < len; i++) {
355
- char ch = str[i];
356
- if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '\f') continue;
357
- if (ch == '=') {
358
- pad++;
359
- continue;
360
- }
361
- if (pad > 0) {
362
- /* Data after padding */
363
- js_free(ctx, out);
364
- JS_FreeCString(ctx, str);
365
- return JS_ThrowSyntaxError(ctx,
366
- "The string to be decoded is not correctly encoded");
367
- }
368
- uint8_t val = b64_decode_table[(uint8_t)ch];
369
- if (val == 255) {
370
- js_free(ctx, out);
371
- JS_FreeCString(ctx, str);
372
- return JS_ThrowSyntaxError(ctx,
373
- "The string to be decoded is not correctly encoded");
374
- }
375
- accum = (accum << 6) | val;
376
- bits += 6;
377
- if (bits >= 8) {
378
- bits -= 8;
379
- out[j++] = (char)((accum >> bits) & 0xFF);
380
- }
381
- }
382
-
450
+ int err = 0;
451
+ size_t j = b64_decode(str, len, out, &err);
383
452
  JS_FreeCString(ctx, str);
453
+ if (err) {
454
+ js_free(ctx, out);
455
+ return JS_ThrowSyntaxError(ctx,
456
+ "The string to be decoded is not correctly encoded");
457
+ }
384
458
 
385
459
  /* Convert decoded bytes to a JS string. Bytes 0x80-0xFF must become
386
460
  * their corresponding Unicode code points (U+0080-U+00FF), which in
package/src/mikrojs.cpp CHANGED
@@ -89,8 +89,9 @@ static void mik__add_exports(JSContext* ctx, JSModuleDef* m, const char* const*
89
89
  }
90
90
 
91
91
  static const char* const sys_exports[] = {
92
- "evalScript", "memoryUsage", "jsMemoryUsage", "gc", "setTime", "uptime",
93
- "restart", "version", "board", "firmware", "deviceId", "activeTimers"};
92
+ "evalScript", "memoryUsage", "jsMemoryUsage", "gc", "setTime",
93
+ "uptime", "restart", "version", "board", "firmware",
94
+ "deviceId", "activeTimers", "unloadNamespace", "isUnloadableNamespace"};
94
95
 
95
96
  static int mik__sys_module_init(JSContext* ctx, JSModuleDef* m) {
96
97
  JSValue ns = JS_NewObjectProto(ctx, JS_NULL);
@@ -542,6 +543,14 @@ int MIK_Loop(MIKRuntime* mik_rt) {
542
543
  if (mik_rt->restart_at_us > 0) {
543
544
  const MIKPlatform* platform = MIK_GetPlatform();
544
545
  if (platform->get_boot_us() >= mik_rt->restart_at_us) {
546
+ /* The grace window has elapsed; take the configured panic action.
547
+ * In deep-sleep mode the timer wake reboots the chip, so the wake
548
+ * IS the restart — and the live --recover window is forfeit by
549
+ * design (the CPU is suspended). If the platform has no deep-sleep
550
+ * hook (hosts), fall through to a plain restart. */
551
+ if (mik_rt->config.panic_mode == MIK_PANIC_DEEP_SLEEP && platform->deep_sleep_us) {
552
+ platform->deep_sleep_us((uint64_t)mik_rt->config.panic_sleep_duration_ms * 1000);
553
+ }
545
554
  platform->restart();
546
555
  }
547
556
  return 0;
package/src/modules.cpp CHANGED
@@ -200,7 +200,7 @@ static JSModuleDef* mik_module_load_from_fs(JSContext* ctx, const char* module_n
200
200
 
201
201
  static JSModuleDef* mik_module_loader_inner(JSContext* ctx, const char* module_name,
202
202
  void* opaque) {
203
- static const char mikrojs_prefix[] = "mikrojs/";
203
+ static const char mikro_prefix[] = "mikro/";
204
204
 
205
205
  /* Virtual modules take priority over builtins — allows host-side JS to
206
206
  * override any native:* C module (e.g. mocking device modules for dev). */
@@ -239,7 +239,7 @@ static JSModuleDef* mik_module_loader_inner(JSContext* ctx, const char* module_n
239
239
  return nullptr;
240
240
  }
241
241
 
242
- if (strncmp(mikrojs_prefix, module_name, strlen(mikrojs_prefix)) == 0) {
242
+ if (strncmp(mikro_prefix, module_name, strlen(mikro_prefix)) == 0) {
243
243
  JSModuleDef* builtin_m = mik__load_builtin(ctx, module_name);
244
244
  if (builtin_m) return builtin_m;
245
245
  /* mik__load_builtin returns NULL either because the module isn't in
@@ -360,39 +360,6 @@ JSModuleDef* mik_module_loader(JSContext* ctx, const char* module_name, void* op
360
360
  #define MIK__PATHSEP '/'
361
361
  #define MIK__PATHSEP_STR "/"
362
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
363
  int js_module_set_import_meta(JSContext* ctx, JSValue func_val, bool use_realpath, bool is_main) {
397
364
  JSModuleDef* m;
398
365
  char buf[PATH_MAX + 16] = {0};
@@ -454,16 +421,6 @@ int js_module_set_import_meta(JSContext* ctx, JSValue func_val, bool use_realpat
454
421
  JS_PROP_C_W_E);
455
422
  }
456
423
 
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
424
  JS_FreeValue(ctx, meta_obj);
468
425
  return 0;
469
426
  }
@@ -698,11 +655,11 @@ static char* mik__resolve_node_modules(JSContext* ctx, const char* base_dir,
698
655
  return NULL;
699
656
  }
700
657
 
701
- /* Anchored names (native:, mikrojs/, @mikrojs/) are never unloaded
658
+ /* Anchored names (native:, mikro/, @mikrojs/) are never unloaded
702
659
  * and don't contribute meaningfully to the import graph for orphan
703
660
  * detection. We skip recording edges where the target is anchored. */
704
661
  static bool mik__is_anchored_name(const char* name) {
705
- return strncmp(name, "native:", 7) == 0 || strncmp(name, "mikrojs/", 8) == 0 ||
662
+ return strncmp(name, "native:", 7) == 0 || strncmp(name, "mikro/", 6) == 0 ||
706
663
  strncmp(name, "@mikrojs/", 9) == 0;
707
664
  }
708
665
 
@@ -726,8 +683,8 @@ static char* mik__module_normalizer_impl(JSContext* ctx, const char* base_name,
726
683
 
727
684
  static const char internal_prefix[] = "native:";
728
685
  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 &&
686
+ // Only built-in modules (native:, mikro/, @mikrojs/) may import native: internals
687
+ if (strncmp(base_name, "native:", 7) != 0 && strncmp(base_name, "mikro/", 6) != 0 &&
731
688
  strncmp(base_name, "@mikrojs/", 9) != 0) {
732
689
  JS_ThrowTypeError(ctx, "Failed to resolve module specifier '%s'", name);
733
690
  return NULL;
@@ -736,7 +693,7 @@ static char* mik__module_normalizer_impl(JSContext* ctx, const char* base_name,
736
693
 
737
694
  if (name[0] != '.' && name[0] != '/') {
738
695
  /* Built-in modules pass through unchanged */
739
- if (strncmp(name, "mikrojs/", 8) == 0 || strncmp(name, "native:", 7) == 0) {
696
+ if (strncmp(name, "mikro/", 6) == 0 || strncmp(name, "native:", 7) == 0) {
740
697
  return js_strdup(ctx, name);
741
698
  }
742
699
  /* Check if this bare specifier matches an external builtin (board/driver
@@ -942,3 +899,32 @@ int mik__unload_module(JSContext* ctx, const char* normalized_name) {
942
899
 
943
900
  return freed_count;
944
901
  }
902
+
903
+ /* Resolve `ns` to the loaded module it is the namespace of, returning its
904
+ * normalized name as a C string (caller frees via JS_FreeCString), or NULL
905
+ * if `ns` is not an object or not a loaded module's namespace. */
906
+ static const char* mik__module_name_from_ns(JSContext* ctx, JSValueConst ns) {
907
+ if (!JS_IsObject(ns)) return nullptr;
908
+ JSModuleDef* m = JS_FindModuleByNamespace(ctx, ns);
909
+ if (!m) return nullptr;
910
+ JSAtom atom = JS_GetModuleName(ctx, m);
911
+ const char* name = JS_AtomToCString(ctx, atom);
912
+ JS_FreeAtom(ctx, atom);
913
+ return name;
914
+ }
915
+
916
+ int mik__unload_namespace(JSContext* ctx, JSValueConst ns) {
917
+ const char* name = mik__module_name_from_ns(ctx, ns);
918
+ if (!name) return 0; /* not a loaded module's namespace: idempotent no-op */
919
+ int rv = mik__unload_module(ctx, name);
920
+ JS_FreeCString(ctx, name);
921
+ return rv;
922
+ }
923
+
924
+ bool mik__is_unloadable_namespace(JSContext* ctx, JSValueConst ns) {
925
+ const char* name = mik__module_name_from_ns(ctx, ns);
926
+ if (!name) return false;
927
+ bool ok = !mik__is_anchored_name(name);
928
+ JS_FreeCString(ctx, name);
929
+ return ok;
930
+ }