src/Pure/System/nodejs.scala
author wenzelm
Sun, 07 Sep 2025 14:10:08 +0200
changeset 83108 75a5089dad12
parent 76510 b0ad975cd25b
child 83114 310f1c728ed2
permissions -rw-r--r--
clarified signature and modules: more explicit Nodejs.setup;

/*  Title:      Pure/System/nodejs.scala
    Author:     Makarius

Support for the Node.js platform, in conjunction with Isabelle/VSCodium.

See also: https://nodejs.org/docs/latest-v22.x/api/index.html
*/

package isabelle


object Nodejs {
  /** independent installation **/

  val default_version = "22.17.0"

  def context(
    platform: Isabelle_Platform = Isabelle_Platform.local,
    version: String = default_version
  ): Context = new Context(platform, version)

  def setup(
    base_dir: Path,
    platform: Isabelle_Platform = Isabelle_Platform.local,
    version: String = default_version,
    packages: List[String] = Nil,
    progress: Progress = new Progress
  ): Directory = {
    context(platform = platform, version = version)
      .setup(base_dir, packages = packages, progress = progress)
  }

  class Context private[Nodejs](val platform: Isabelle_Platform, version: String) {
    override def toString: String =
      "node-" + version + "-" + platform.ISABELLE_PLATFORM(windows = true, apple = true)

    def arch: String = if (platform.is_arm) "arm64" else "x64"

    def platform_name: String =
      if (platform.is_windows) "win" else if (platform.is_macos) "darwin" else "linux"

    def full_name: String = "node-v" + version + "-" + platform_name + "-" + arch

    def download_ext: String = if (platform.is_windows) "zip" else "tar.gz"

    def download_url: String =
      "https://nodejs.org/dist/v" + version + "/" + full_name + "." + download_ext

    def setup(
      base_dir: Path,
      packages: List[String] = Nil,
      progress: Progress = new Progress
    ): Directory = {
      Isabelle_System.with_tmp_file("node", ext = download_ext) { archive =>
        progress.echo("Getting Node.js ...")
        Isabelle_System.download_file(download_url, archive)

        progress.echo("Installing node ...")
        Isabelle_System.extract(archive, base_dir)
        val node_dir = new Directory(this, base_dir + Path.basic(full_name))

        for (name <- packages) node_dir.install(name, progress = progress)

        node_dir
      }
    }
  }

  class Directory private[Nodejs](val context: Context, val path: Path) {
    override def toString: String = path.toString

    def bin: Path = path + Path.basic("bin")
    def path_setup: String = "export PATH=" + File.bash_path(bin) + """:"$PATH""""

    def install(name: String, progress: Progress = new Progress): Unit = {
      progress.echo("Installing " + name + " ...")
      Isabelle_System.bash(path_setup + "\nnpm install -g " + Bash.string(name), cwd = path).check
    }
  }



  /** source snippets **/

  /* require modules */

  def require_module(name: JS.Source, module: JS.Source): JS.Source =
    name + " = require(" + module + ")"

  def require_path(name: JS.Source, path: Path, dir: Boolean = false): JS.Source =
    require_module(name, JS.platform_path(path, dir = dir))

  def require_builtin(name: String): JS.Source =
    require_module("const " + name, JS.string(name))


  /* file-system operations */

  def require_fs: JS.Source = require_builtin("fs")

  val encoding_utf8: JSON.T = JSON.Object("encoding" -> "utf8")

  def read_file(path: Path): JS.Source =
    JS.function("fs.readFileSync", JS.platform_path(path), JS.value(encoding_utf8))

  def write_file(path: Path, arg: JS.Source): JS.Source =
    JS.function("fs.writeFileSync", JS.platform_path(path), arg, JS.value(encoding_utf8))


  /* external process */

  def execute(js: String): Process_Result =
    Isabelle_System.bash("isabelle node -", input = js,
      description = "Node.js").check
}