v0.6.0 (latest)
Changes (since previous release)
fix:
Denox.Deps.add_to_config/3andremove_from_config/2now return{:error, "Failed to write ..."}(human-readable string) instead of{:error, :eacces}(raw posix atom) when the config file is not writabletest: add
Denox.Deps.add/3write failure test — verifies that readonly config files produce a readable error string, not a raw posix error atomtest: add
Denox.Deps.runtime/1empty import_map branch coverage — exercises themap_size == 0conditional skip in the runtime buildertest: add
CallbackHandlernon-JSON-serializable return value test — verifies that when a callback function returns a value that cannot be JSON-encoded (e.g., a PID), therescueclause catches theJSON.EncodeErrorand sends it to JS as an error tuple rather than crashing the handlertest: add
stream/1RuntimeError test — verifies that whenstart_linkfails (e.g., neither:filenor:packageprovided), the stream raisesRuntimeErrorwhen enumerated, as documented in the@docwarning blockfix(test): replace flaky
Application.stop(:inets)test in coverage_gaps_test.exs with a safe synchronous test — the dangerous global state mutation was intermittently causing TLS errors in concurrently running async test modulestest: add
Denox.CLI.find_deno/0system PATH success test — verifies system deno binary is found and returned as{:ok, path}when present on PATH, taking priority over the bundled CLI (exercises the first branch of the 3-step fallback chain)test: add
Denox.CLI.handle_response/2edge case coverage —:exitreason tuples (e.g., connection refused caught bydownload/2) and non-200 responses with empty body are both handled and produce readable error messagestest: add
unsubscribe/1idempotency tests — unsubscribing a PID that was never subscribed, and calling unsubscribe twice after one subscribe, both return:okwithout error (verified againstDenox.Run.Base.__handle_call__/4logic)test: add
stream_from/2edge case — empty list returned when server exits immediately without producing any output (complementsstream/1no-output test)fix(rust): log unexpected I/O errors in stdout pipe reader thread instead of silently discarding them; normal pipe-close errors (BrokenPipe, UnexpectedEof) remain silent as they are expected on runtime exit
refactor: extract
Denox.Permissionsmodule to centralize NIF permission JSON building — eliminates duplicate@valid_permission_keysandbuild_permissions_jsonbetweenDenoxandDenox.Runrefactor: deduplicate
ts_extension?/1betweenDenoxandDenox.Pooltest: add unit tests for
Denox.Permissions(8 tests covering all code paths)test: add permission enforcement tests verifying Deno actually restricts env and file read access under
permissions: :noneand allows under:all/ granularfix: tag deno-dependent tests with
:denoto skip in CIfix(ci): set
MIX_ENV=testfor E2E workflowrefactor: replace JS polyfill I/O with native
deno_iopipe bridging inruntime_runrefactor: extract
RuntimeRunResourceand NIF functions intoruntime_run.rsfix: add panic safety (
catch_unwind) to runtime threadschore: add
@specannotations to callback implementations inDenox.Run,Denox.CLI.Run, andDenox.CallbackHandlerchore: add
@specto injected GenServer callbacks (init/1,handle_call/3,dispatch_line/2,handle_exit/2) inDenox.Run.Basechore: add
@specto private helpers inDenox.PoolandDenox.CLI.Runfeat: validate unknown granular permission keys in
Denox.runtime/1andDenox.Run.start_link/1— raisesArgumentErrorinstead of silently ignoringfeat: move permission validation before CLI lookup in
Denox.CLI.Run.init_backend/1so callers getArgumentErrorbefore any binary searchtest: add
deny_*permission tests for bothDenox.Run(NIF) andDenox.CLI.Run(subprocess) backendstest: add Pool error-path tests for
eval_file,eval_file_decode,eval_module,exec,exec_ts, andcall/call_asyncwith undefined functionstest: add
with_runtime/2error-passthrough test (user function returning{:error, ...})docs: document
deny_*keys andArgumentErrorinDenox.runtime/1:permissionsoptiondocs: expand
Denox.Poolmoduledoc withload_npmandexecusage patternsdocs: clarify
exec/2andexec_ts/2: errors must still be handled (not fire-and-forget)docs: document
with_runtime/2error-passthrough semanticsdocs: clarify
Denox.CLI.Runprimary use case is npm/jsr packagesfix:
Denox.Pool.init/1now returns{:stop, {:failed_to_create_runtime, msg}}instead of raising when runtime creation fails — enables proper supervision tree error propagationfix:
Denox.Run.Basesubscribe/1now deduplicates by PID — calling subscribe twice from the same process no longer creates duplicate monitor refs or sends duplicate stdout messagesfix:
Denox.Permissions.to_nif_json/1now raisesArgumentErroron unknown permission keys even when the value isfalse(previously silently ignored)test: add duplicate subscribe idempotency test
test: add Pool init failure propagation test
test: add CLI.Run invalid env type validation test
test: add permission edge case tests (mixed allow/deny, empty lists, false unknown keys)
Improvements
Denox.Runstdin/stdout now uses native OS pipe bridging viadeno_io::Stdio/StdioPipeinstead of JS-level polyfills. This eliminates the 10ms busy-poll loop for stdin reads, removes theconsole.logoverride, and implements the PRD-specified 2N+1 thread model (event loop + pipe reader + pipe writer).Denox.Run.Basenow has a proper@moduledocdocumenting the shared behaviour contract (init_backend/1,send_backend/2,stop_backend/1,alive_backend?/1) for backend implementors.Denox.Rundocumentation now explicitly states thatnpm:andjsr:specifiers are not supported by the NIF backend (TsModuleLoader only handlesfile://,https://, andhttp://schemes). Users are directed toDenox.CLI.Runfor npm/jsr packages.- Test coverage expanded: pipe I/O edge cases (empty lines, Unicode,
Deno.stdout.write, long lines, rapid output), Web Streams API,Blob,MessageChannel, Promise combinators,setTimeout/setInterval, granular permissions enforcement. - PRD checklist fully verified (29/29 items passing).
capture/1convenience function added toDenox.Run.Base(and inherited by bothDenox.RunandDenox.CLI.Run): starts a runtime, collects all stdout lines until exit or timeout, and returns them as{:ok, [String.t()]}. Uses the recv-poll pattern to avoid subscribe race conditions on fast-completing scripts.stream/1convenience function added toDenox.Run.Base: returns a lazyStream.resource/3enumerable that yields stdout lines one-by-one, suitable for large or streaming output. Halts on process exit or timeout.send_and_recv/3convenience function added toDenox.Run.Base: one-shot request/response helper for JSON-RPC over stdio (e.g. MCP servers). Sends a line and returns the next line, wrappingsend/2+recv/2in a single call.stream_from/2convenience function added toDenox.Run.Base: returns a lazyStream.resource/3enumerable from an already-running server PID. Unlikestream/1, it does not start or stop the server — the caller retains ownership. Pairs naturally withwith_runtime/2for request-response workflows.with_runtime/2bracket-style resource management added toDenox.Run.Base: starts a runtime, passes the PID to a user function, and guarantees cleanup via anafterblock — even if the function raises. Returns{:error, reason}if the runtime fails to start.
v0.5.0 — 2026-03-22
Features
Denox.RunNIF backend — replaced subprocessdeno runwith an in-processdeno_runtimeMainWorker via Rustler NIF. No externaldenobinary required forDenox.Run. Supports stdin/stdout streaming, telemetry, OTP supervision, and all permission modes.Denox.CLIbinary manager — downloads and caches the official Deno binary for the current platform (macOS/Linux, x86_64/aarch64). Configure withconfig :denox, :cli, version: "2.x.x"and runmix denox.cli.install.Denox.CLI.Run— subprocess-based runner using the bundled CLI binary; same API asDenox.Runbut spawns adenoprocess per instance. Useful when full CLI features are needed.- Shared
Run.Basebehaviour — shared GenServer dispatch logic extracted as a__using__macro behaviour, supporting both NIF and CLI backends with a commonsend/recv/subscribe/unsubscribe/alive?/stopAPI. - Granular permissions — both
Denox.RunandDenox.CLI.Runsupport:all,nil/:none, or a keyword list ofallow_*/deny_*permission flags. - Telemetry —
[:denox, :run, :start],[:denox, :run, :stop],[:denox, :run, :recv]events emitted for all backends.
Fixes
deno_corereplaced withdeno_runtimeMainWorker for full Deno API compatibility (fetch, timers, Deno.env, etc.)- Monitor leak on
unsubscribe/1: monitors are now properly demonitored when subscribers unsubscribe - Stale
recv_waiterson timeout: timed-outrecv/2callers are now monitored; dead waiters are removed before dispatching lines mix denox.runnow usesDenox.CLI.find_deno()for bundled CLI fallback, consistent withDenox.DepsandDenox.Npmmix denox.rundrains remaining port messages after exit_status to prevent dropping final output without trailing newline (race condition on Linux where{:exit_status, 0}arrives before{:noeol, chunk})Denox.Deps.ensure_vendor_config/1refactored to reduce nesting depth- File write errors in
Denox.Deps.ensure_vendor_confignow include the filename in the error message send/2documentation updated to document automatic newline appending and{:error, :closed}returnDenox.CLI.Runnow documents the:deno_flagsoption for passing extra flags todeno run
v0.4.1
Features
- Added 206 comprehensive web/Node.js global polyfills
- Added
globalThis.fetchpolyfill with Headers/Request/Response
Fixes
- Added
:mixto Dialyzer PLT apps for Mix.Task modules - Added credo and dialyxir deps, resolved all credo issues
- Added Rust toolchain and
DENOX_BUILDto CI workflow - Fixed hello script and cleaned up .envrc
v0.4.0
Features
Denox.Runsubprocess runner — GenServer wrappingdeno runwith bidirectional stdio, enabling Elixir apps to run full Deno programs (MCP servers, CLI tools) with OTP supervisionmix denox.runtask — drop-in replacement fordeno runwith stdin forwarding, e.g.mix denox.run -A @modelcontextprotocol/server-github- Deno permission mapping —
:allfor-A, or granular keyword list (allow_net,allow_env, etc.) - Auto npm: prefix — bare
@scope/namespecifiers are automatically prefixed withnpm: - Pub/sub stdout —
Denox.Run.subscribe/1for receiving{:denox_run_stdout, pid, line}messages - Real
setTimeout— nativeop_sleepasync op for millisecond-accurate delays insetTimeout/setInterval - Convenience functions — added
eval_async_decode,eval_ts_async_decode,eval_file_async,eval_file_decode,eval_file_async_decodefor cleaner API
Fixes
- Fixed
setTimeoutreturning immediately instead of waiting for the specified delay
v0.3.0
Breaking Changes
Denox.eval_async/2,Denox.eval_ts_async/2,Denox.call_async/3,Denox.call_async_decode/3now returnTask.t()instead of{:ok, result} | {:error, message}. CallDenox.await(task)to get the result.Denox.eval/2now pumps the event loop (supports Promises and dynamic imports in sync mode)
Features
- Unified event loop pumping — all eval modes now pump the event loop, making sync and async consistent
- Task-based async API — true concurrency with proper task management
Denox.await/2helper — delegates toTask.awaitfor convenience
Playground Enhancements
- Added Preact JSX SSR demo with real JSX syntax support
- Added import map example showing bare specifier resolution
- Display import map content above code editor
- Fixed async examples to handle long CDN fetches without timing out
v0.2.2
Features
- JSX/TSX transpilation — changed from
MediaType::TypeScripttoMediaType::Tsxfor native JSX support setTimeoutpolyfill — async-safe timer polyfill for microtask-based delays- Preact SSR example — server-side rendering via
preact-render-to-string
Playground
- 12 example snippets including Preact JSX SSR demo
- CDN imports from esm.sh, esm.run, jsr
- Zod schema validation example
v0.2.1
Features
- Multiple CDN examples — lodash, Zod, JSR packages
- Import map support —
Denox.runtime(import_map: %{...})for bare specifier resolution - Timer polyfill — basic
setTimeout/setInterval(microtask-based, no real delays)
Playground
- 10 example snippets
- Language toggle (JS/TS)
- Mode toggle (Sync/Async)
- Execution history with accordion view
- Theme switcher
v0.1.0
Initial release.
Features
- JavaScript evaluation — sub-millisecond V8 eval via
deno_core0.311 - TypeScript transpilation — native swc/deno_ast, transpile-only (no type-checking)
- ES module loading —
import/exportbetween.ts/.jsfiles witheval_module/2 - Async evaluation —
await, dynamicimport(), Promise resolution viaeval_async/2 - CDN imports — fetch from esm.sh, esm.run, etc. with in-memory + disk caching
- Dependency management —
deno.json+mix denox.installfor npm/jsr packages - Pre-bundling —
mix denox.bundleandDenox.Npm.load/2for self-contained JS files - Runtime pool —
Denox.PoolGenServer with round-robin across N V8 isolates - Import maps — bare specifier resolution via
:import_mapoption - JS → Elixir callbacks —
Denox.callback()in JS,Denox.CallbackHandlerGenServer - V8 snapshots —
Denox.create_snapshot/2for faster cold starts - Sandbox mode — strip extensions for reduced attack surface
Telemetry —
[:denox, :eval, :start | :stop | :exception]events- Precompiled NIFs — via RustlerPrecompiled for macOS and Linux (x86_64, aarch64)
API
Denox.runtime/1— create a V8 runtime with optionsDenox.eval/2,Denox.eval_ts/2— synchronous JS/TS evaluationDenox.eval_async/2,Denox.eval_ts_async/2— async evaluation (event loop)Denox.exec/2,Denox.exec_ts/2— evaluate, ignore return valueDenox.call/3,Denox.call_async/3— call named JS functionsDenox.eval_decode/2,Denox.eval_ts_decode/2,Denox.call_decode/3,Denox.call_async_decode/3— evaluate and decode JSONDenox.eval_module/2— load ES module filesDenox.eval_file/3— read and evaluate JS/TS filesDenox.create_snapshot/2— create V8 snapshotsDenox.Pool— runtime pool for concurrent workloadsDenox.CallbackHandler— JS → Elixir callback handlerDenox.Deps— dependency management via deno CLIDenox.Npm— pre-bundled npm package loading