feat(rtl): nodertl — pure-Go Node.js bridge (NODE_EVAL)

Lets PRG use the npm ecosystem with NO C/CGO/Harbour native — faithful to
Five (C→Go). NODE_EVAL(cJs[,cWorkdir[,cInput]]) shells out to `node -e`
(pure os/exec), resolving node from PATH or common Homebrew/usr paths;
user data passes via env FIVE_NODE_INPUT (no code injection); returns
{ok,out,err} JSON. The Go binary stays pure Go and only spawns node when
this RTL is used.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
CharlesKWON
2026-06-15 17:29:11 +09:00
parent 8ddd6abc69
commit 9ec3063418

View File

@@ -0,0 +1,86 @@
// hbrtl_ext/nodertl — 순수 Go 로 Node.js 서브프로세스를 호출해 npm 생태계를
// PRG 에서 쓰게 하는 fivenode 런타임 RTL. C/CGO/Harbour 네이티브 한 줄 없이
// (Five = C→Go 재구현 철학 유지) Node 의 전체 라이브러리 생태계에 접근한다.
//
// Five(Go 바이너리) ──exec──▶ node -e <js> ──require──▶ npm 모듈
//
// PRG surface:
// NODE_EVAL(cJs [, cWorkdir [, cInput]]) -> cJson
// cJs : 실행할 JS (결과를 process.stdout 으로 출력)
// cWorkdir : require() 해석 기준 디렉터리(node_modules 위치). 비우면
// 환경변수 SOLMADE_NODE_DIR 사용.
// cInput : 사용자 입력. JS 에 코드로 박지 말고 env FIVE_NODE_INPUT 으로
// 전달된다(코드 인젝션 방지). JS 에서 process.env.FIVE_NODE_INPUT 로 읽음.
// 반환: {"ok":bool,"out":string,"err":string} JSON 문자열.
package nodertl
import (
"bytes"
"encoding/json"
"os"
"os/exec"
"five/hbrt"
)
func init() {
hbrt.HB_FUNC("NODE_EVAL", nodeEval)
}
// nodeBin: PATH 에 node 가 없어도(예: launchd 의 좁은 PATH) 흔한 위치를 폴백.
func nodeBin() string {
if p, err := exec.LookPath("node"); err == nil {
return p
}
for _, p := range []string{"/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node"} {
if _, err := os.Stat(p); err == nil {
return p
}
}
return ""
}
func resJSON(ok bool, out, errs string) string {
b, _ := json.Marshal(map[string]interface{}{"ok": ok, "out": out, "err": errs})
return string(b)
}
func nodeEval(ctx *hbrt.HBContext) {
if ctx.PCount() < 1 || !ctx.IsChar(1) {
ctx.RetC(resJSON(false, "", "NODE_EVAL(cJs[,cWorkdir[,cInput]]) — missing js"))
return
}
js := ctx.ParC(1)
workdir := ""
if ctx.PCount() >= 2 && ctx.IsChar(2) {
workdir = ctx.ParC(2)
}
if workdir == "" {
workdir = os.Getenv("SOLMADE_NODE_DIR")
}
input := ""
if ctx.PCount() >= 3 && ctx.IsChar(3) {
input = ctx.ParC(3)
}
bin := nodeBin()
if bin == "" {
ctx.RetC(resJSON(false, "", "node not found (install node; or set PATH)"))
return
}
cmd := exec.Command(bin, "-e", js)
if workdir != "" {
cmd.Dir = workdir
}
cmd.Env = append(os.Environ(), "FIVE_NODE_INPUT="+input)
var out, errb bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &errb
runErr := cmd.Run()
ctx.RetC(resJSON(runErr == nil, out.String(), errb.String()))
}