Skip to content

test-emu avr8js: ERR_MODULE_NOT_FOUND because NODE_PATH is CJS-only (ESM ignores it) #291

Description

@zackees

Symptom

Every fbuild test-emu --emulator avr8js invocation fails with:

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'avr8js' imported from /tmp/.tmpXXXXXX/headless.mjs
    at ModuleLoader.resolveSync (node:internal/modules/esm/loader:746:52)
    ...
avr8js ATmega328P test-emu failed: failed: avr8js exited with code 1

Reproducible in FastLED master CI on every run, e.g. https://github.com/FastLED/FastLED/actions/runs/26677701986

The avr8js npm package is correctly installed at ~/.fbuild/cache/avr8js-node/node_modules/avr8js/ (the "belt-and-suspenders" step in the FastLED workflow even runs npm install avr8js@0.21.0 upfront).

Root cause

crates/fbuild-daemon/src/handlers/emulator/runners.rs (line ~176):

let session_dir = tempfile::TempDir::new()?;
let script_path = session_dir.path().join("headless.mjs");
std::fs::write(&script_path, AVR8JS_HEADLESS_MJS)?;

The script is written to /tmp/.tmpXXX/headless.mjs. headless.mjs uses a bare ESM specifier:

import { CPU, avrInstruction, AVRIOPort, ... } from "avr8js";

run_avr8js_headless then spawns Node with:

.env("NODE_PATH", avr8js_cache_dir.join("node_modules"))

NODE_PATH is ignored by Node's ESM resolver — it only works for CommonJS require(). The ESM resolver walks upward from the script's directory looking for node_modules. From /tmp/.tmpXXX/, it never finds one.

Same bug also exists in crates/fbuild-daemon/src/handlers/emulator/avr8js_deploy.rs:211, which writes headless.mjs into a per-project .fbuild/emulators/avr8js/<env>/<session_id>/ directory — also too deep to ever reach the cache's node_modules.

Proposed fix

Write headless.mjs inside the avr8js cache dir (which already has node_modules/avr8js/ next to it) instead of in an unrelated tempdir. The script content is a compile-time constant (include_str!), so it's safe to write/overwrite. Update both call sites:

runners.rs (test-emu / EmulatorRunner path):

// NODE_PATH does not work for ESM bare specifiers. Stage the script
// inside the cache dir so its node_modules/avr8js is on the resolver
// walk-up path. Content is constant; concurrent runs may race the
// write harmlessly.
let script_path = avr8js_cache.join("headless.mjs");
std::fs::write(&script_path, AVR8JS_HEADLESS_MJS)?;

avr8js_deploy.rs (POST /api/deploy path):

Same treatment — write script_path inside avr8js_cache rather than session_dir. The session_dir can keep firmware.hex / session.json; only the script needs to live with node_modules.

Optionally drop the dead NODE_PATH env var from avr8js_headless.rs since it never helped anyway (or keep it for the CommonJS-side belt-and-suspenders).

Test plan

  • Unit test: assert script_path.parent() == avr8js_cache_dir after the change.
  • Integration: re-run FastLED's uno_avr8js_test workflow — should reach Test loop and succeed.

Repro

Locally on Linux (no fbuild dev box needed):

mkdir /tmp/no-nodeMods && cd /tmp/no-nodeMods
echo 'import "avr8js"; console.log("ok");' > t.mjs
NODE_PATH=$HOME/.fbuild/cache/avr8js-node/node_modules node t.mjs
# → ERR_MODULE_NOT_FOUND

# Same script run from the cache dir works:
cp t.mjs ~/.fbuild/cache/avr8js-node/t.mjs
node ~/.fbuild/cache/avr8js-node/t.mjs
# → ok

Filed automatically while triaging broken FastLED CI builds; intent is to open a PR immediately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions