docs(rag): §7 npm path = handle-based nodebridge (FN_REQUIRE), pure Go

Replace the NODE_EVAL example with the faithful C++-fivenode Require()
port: FN_REQUIRE/FN_CALL/FN_END over a persistent node sidecar, pure-Go
glue, no C. Live demo at solmade.kr/qrcode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
CharlesKWON
2026-06-15 19:04:09 +09:00
parent 4ce927a8de
commit af5150211e

View File

@@ -217,30 +217,34 @@ module, blank-import with `--rtl`, call the registered function. Single static b
no runtime deps. Use whenever a good Go library exists (Postgres, PDF, XLSX,
crypto/rand, argon2, bluemonday).
**(b) npm package via the pure-Go Node bridge (`nodertl`).** When the library you want
is npm-only (QR, charting, JS SDKs), call Node from Go as a **subprocess** — no CGO,
no C addon. RTL lives in `fivenode_go/hbrtl_ext/nodertl`:
**(b) npm package via the handle-based Node bridge (`nodebridge`).** The original
C++ fivenode was `Harbour ⇄ C glue ⇄ Node(npm)` — JS always ran in real Node, the C was
only a JSON glue. The faithful Go port replaces the *glue* (not Node) with Go:
`Five(Go) ⇄ nodebridge(Go) ⇄ persistent node ⇄ npm`. Same handle protocol as the C++
version (`require` → handle, `call` → method with auto-await, `end`), **no C/CGO**, and
**not** `node -e` string eval. RTL lives in `fivenode_go/hbrtl_ext/nodebridge`:
```five
// NODE_EVAL(cJs [, cWorkdir [, cInput]]) -> {"ok","out","err"} JSON
// cWorkdir = dir holding node_modules; cInput → env FIVE_NODE_INPUT (no code injection)
LOCAL cJs := "const qr=require('qrcode');" + ;
"qr.toString(process.env.FIVE_NODE_INPUT,{type:'svg'})" + ;
".then(s=>process.stdout.write(s)).catch(e=>{process.stderr.write(String(e));process.exit(1);});"
LOCAL hRes := hb_jsonDecode( NODE_EVAL( cJs, hb_GetEnv("SOLMADE_NODE_DIR","./node"), cUrl ) )
IF hb_HGetDef( hRes, "ok", .f. )
cSvg := hb_HGetDef( hRes, "out", "" )
// FN_REQUIRE(cModule) -> nHandle ; FN_CALL(nH,cMethod[,cArgsJson]) -> {ok,type,value,err}
LOCAL nH := FN_REQUIRE( "qrcode" ) // npm module handle (persistent)
LOCAL cArgs := hb_jsonEncode( { cUrl, { "type" => "svg", "width" => 280 } } ) // JSON arg array
LOCAL hR := hb_jsonDecode( FN_CALL( nH, "toString", cArgs ) ) // async method auto-awaited
FN_END( nH )
IF hb_HGetDef( hR, "ok", .f. )
cSvg := hb_HGetDef( hR, "value", "" ) // type: string|number|boolean|json|buffer(b64)
ENDIF
```
- Setup: a `node/` dir with `package.json` + `npm install <pkg>`; point cWorkdir there.
- The JS prints its result to `process.stdout`; pass user data via `cInput`
(env `FIVE_NODE_INPUT`), never by string-concatenating into the JS.
- The Go binary stays pure Go and only spawns `node` when this RTL is called.
- Real example: solmade `app/api/article_qr.prg` → npm `qrcode` → SVG, verified.
- Setup: a `node/` dir with `package.json` + `npm install <pkg>`; the bridge points
`NODE_PATH` there (default dir via env `SOLMADE_NODE_DIR`).
- One persistent `node` per process; calls are serialized; the Go binary stays pure Go
and only spawns node when `FN_*` is used. `FN_LASTERROR()` for diagnostics.
- Args are a JSON array (`hb_jsonEncode`); user data never concatenated into JS code.
- Real example + live demo: solmade `app/api/qr.prg` → npm `qrcode` → SVG, served at
`solmade.kr/qrcode`.
**Legacy (do NOT build new on it):** the original `fivenode` (Node + N-API/koffi calling
**C Harbour**) exposed `Require("pkg")`/`aWait()` directly in PRG. That path needs a C
Harbour native lib (`libfivenode`) — against the C→Go goal — so prefer (b).
**Legacy (do NOT build new on it):** the original `fivenode` used a **C** N-API/koffi
addon (`libfivenode`) for the same `Require()`/`aWait()` protocol — against the C→Go
goal. `nodebridge` reproduces that protocol with pure-Go glue instead.
**Choose (b) over (a)** only when the npm package has no good Go equivalent or would be
heavy to reimplement; the cost is a Node runtime dependency for that feature.