From 9ec3063418d91dbbd448f9e1cf7c25b11759c61c Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Mon, 15 Jun 2026 17:29:11 +0900 Subject: [PATCH] =?UTF-8?q?feat(rtl):=20nodertl=20=E2=80=94=20pure-Go=20No?= =?UTF-8?q?de.js=20bridge=20(NODE=5FEVAL)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- hbrtl_ext/nodertl/nodertl.go | 86 ++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 hbrtl_ext/nodertl/nodertl.go diff --git a/hbrtl_ext/nodertl/nodertl.go b/hbrtl_ext/nodertl/nodertl.go new file mode 100644 index 0000000..669d3c1 --- /dev/null +++ b/hbrtl_ext/nodertl/nodertl.go @@ -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 ──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())) +}