#lang racket/base (require racket/file racket/list racket/match racket/port racket/string) (provide js-engine? js-engine-name js-engine-path js-engine-kind js-engine-version js-run-result? js-run-result-engine js-run-result-status js-run-result-stdout js-run-result-stderr js-run-result-success? known-js-engine-names find-js-engine run-js-file) (struct js-engine (name path kind version-args) #:transparent) (struct js-run-result (engine status stdout stderr) #:transparent) (define (path-string/non-empty? v) (and (string? v) (not (string=? v "")))) (define (executable-file? p) (and p (file-exists? p))) (define (getenv/path name) (define v (getenv name)) (and (path-string/non-empty? v) (string->path v))) (define engine-specs ;; name aliases kind version-args extra-paths '((node ("node" "nodejs") plain ("--version") ("/opt/nvm/versions/node/v22.16.0/bin/node" "/usr/local/bin/node" "/usr/bin/node")) (deno ("deno") plain ("--version") ()) (bun ("bun") plain ("--version") ()) (qjs ("qjs" "quickjs") plain ("-v") ()) (d8 ("d8") plain ("--version") ()) (jsc ("jsc") plain ("--version") ()) (js ("js") plain ("--version") ()) (chromium ("chromium" "chromium-browser" "google-chrome" "google-chrome-stable") chromium ("--version") ("/usr/bin/chromium" "/usr/bin/chromium-browser" "/usr/bin/google-chrome")))) (define (known-js-engine-names) (map car engine-specs)) (define (lookup-engine-spec name) (define wanted (string-downcase (format "~a" name))) (for/first ([spec (in-list engine-specs)] #:when (or (string=? wanted (symbol->string (car spec))) (member wanted (cadr spec)))) spec)) (define (candidate-paths spec) (match spec [(list name aliases kind version-args extra-paths) (append (if (eq? name 'node) (filter values (list (getenv/path "JSMAKER_NODE"))) '()) (filter values (list (getenv/path "JSMAKER_ENGINE_PATH"))) (filter values (map find-executable-path aliases)) (map string->path extra-paths))])) (define (make-engine-from-spec spec) (match spec [(list name aliases kind version-args extra-paths) (for/first ([p (in-list (candidate-paths spec))] #:when (executable-file? p)) (js-engine name p kind version-args))])) (define (find-js-engine [requested (or (getenv "JSMAKER_ENGINE") "auto")]) (define req (string-downcase requested)) (cond [(or (string=? req "") (string=? req "auto")) (define browser-fallback? (getenv "JSMAKER_BROWSER_FALLBACK")) (or (for*/first ([spec (in-list engine-specs)] #:unless (and (not browser-fallback?) (eq? (car spec) 'chromium)) [engine (in-value (make-engine-from-spec spec))] #:when engine) engine) #f)] [else (define spec (lookup-engine-spec req)) (and spec (make-engine-from-spec spec))])) (define (capture-timeout-seconds) (define v (getenv "JSMAKER_ENGINE_TIMEOUT_SECONDS")) (cond [(and (path-string/non-empty? v) (string->number v)) => values] [else 15])) (define (capture path args) (with-handlers ([exn:fail? (lambda (e) (values 127 "" (exn-message e)))]) (define-values (proc stdout stdin stderr) (apply subprocess #f #f #f path args)) (define waiter (thread (lambda () (subprocess-wait proc)))) (define finished? (sync/timeout (capture-timeout-seconds) waiter)) (unless finished? (subprocess-kill proc #t) (thread-wait waiter)) (values (if finished? (subprocess-status proc) 124) (port->string stdout) (string-append (port->string stderr) (if finished? "" "\nJavaScript engine timed out and was killed.\n"))))) (define (js-engine-version engine) (define-values (status stdout stderr) (capture (js-engine-path engine) (js-engine-version-args engine))) (define s (string-trim (if (string=? stdout "") stderr stdout))) (and (zero? status) (not (string=? s "")) s)) (define (plain-run-args engine js-path) (case (js-engine-name engine) [(deno) (list "run" "--quiet" js-path)] [else (list js-path)])) (define (copy-js-as-browser-html js-path) (define html-path (build-path (find-system-path 'temp-dir) (format "jsmaker-browser-~a.html" (current-inexact-milliseconds)))) (define js (file->string js-path)) (call-with-output-file html-path #:exists 'replace (lambda (out) (displayln "
" out)
      (displayln "" out)))
  html-path)

(define (chromium-run engine js-path)
  (define html-path (copy-js-as-browser-html js-path))
  (define url (string-append "file://" (path->string html-path)))
  (define args (list "--headless" "--disable-gpu" "--no-sandbox" "--dump-dom" url))
  (define-values (status stdout stderr) (capture (js-engine-path engine) args))
  (define browser-fail? (regexp-match? #rx"data-jsmaker-status=\"fail\"" stdout))
  (js-run-result engine (if browser-fail? 1 status) stdout stderr))

(define (run-js-file engine js-path)
  (case (js-engine-kind engine)
    [(chromium) (chromium-run engine js-path)]
    [else
     (define-values (status stdout stderr)
       (capture (js-engine-path engine) (plain-run-args engine js-path)))
     (js-run-result engine status stdout stderr)]))

(define (js-run-result-success? result)
  (zero? (js-run-result-status result)))