R/tailwind_cli.R

Defines functions compile_tailwindcss is_tailwindcss_installed get_cli_executable install_tailwindcss_cli

Documented in compile_tailwindcss install_tailwindcss_cli is_tailwindcss_installed

#' Installs the 'TailwindCSS' CLI
#'
#' @description This will download the 'TailwindCSS' standalone CLI to the
#' current working directory.
#'
#' @details This will download the 'TailwindCSS' standalone CLI to the current
#'   working directory.
#'   See [here](https://tailwindcss.com/blog/standalone-cli) for details on the
#'   standalone CLI. This saves you from having to install 'node.js'.
#'
#'   On the mac, after installing the CLI, you need to make sure that the file
#'   is executable to run it. For Mac, the easiest way to do so is to ensure
#'   you're in the correct working directory in R and type
#'   `system("chmod +x tailwindcss")`.
#'   Alternatively, you could `cd` to the directory in terminal and then run
#'   `chmod +x tailwindcss`.
#'
#'
#' @param overwrite if existing installations should be overwritten
#' @param version the version to install, default is latest
#' @param verbose if the version etc should be reported
#'
#' @export
#' @return invisibly the path to the cli program
#'
#' @seealso [compile_tailwindcss]
#' @examples
#' if (interactive()) {
#'   install_tailwindcss_cli()
#' }
install_tailwindcss_cli <- function(
  overwrite = FALSE,
  version = "latest",
  verbose = FALSE
) {
  if (is_tailwindcss_installed() && !overwrite) {
    stop("Found existing tailwindcss installation. Abort installation!")
  }

  info <- Sys.info()
  # 1) find system, either linux, macos, or windows
  sys <- tolower(info[["sysname"]])
  # TODO: not sure if this is catches all Mac OS?!
  if (sys == "darwin") sys <- "macos"

  # 2) find architecture
  # TODO: does this distinguish between x64 and ARM64 in all cases?
  arch <- if (grepl("x86.64", info[["machine"]])) "x64" else "arm64"

  file <- paste(
    "tailwindcss",
    if (sys == "windows") {
      "windows-x64.exe"
    } else {
      paste(sys, arch, sep = "-")
    },
    sep = "-"
  )

  # 3) get latest release version
  url <- "https://github.com/tailwindlabs/tailwindcss/releases"
  if (version == "latest") {
    html <- readLines(url)
    # Returns all available versions
    v <- grep(
      ".*\\>(v[0-9]+.[0-9]+.[0-9]+)\\<.*",
      value = TRUE,
      html,
      perl = TRUE
    )
    # Extract release version
    version <- regmatches(v, gregexec("v[0-9]+.[0-9]+.[0-9]+", v))[[1]][1, 1]
  }
  if (verbose) {
    cat(paste0(
      "Trying to download tailwindcss CLI version ",
      version,
      "\n",
      "  from ",
      url,
      "\n"
    ))
  }
  unlink(file)
  download_url <- paste0(url, "/download/", version, "/", file)
  suppressWarnings({
    a <- try(utils::download.file(download_url, file), silent = TRUE)
  })

  if (inherits(a, "try-error")) {
    stop(sprintf(
      paste0(
        "Could not download tailwindcss CLI.\n",
        "  Either tailwindcss CLI version %s could not be found or another error occured.\n",
        "  Please make sure that the version is available from\n  %s"
      ),
      version,
      url
    ))
  }

  # 4) rename file to tailwindcss(.exe)
  target <- "tailwindcss"
  if (sys == "windows") target <- paste0(target, ".exe")
  file.rename(file, target)

  if (sys == "macos") system("chmod +x tailwindcss")

  cat(sprintf(
    paste0(
      "Success: installed tailwindcss version %s as '%s'!\n",
      "Next you must ensure that the tailwind css file is executable.\n",
      "Type `?install_tailwindcss_cli` to read more about how to do this"
    ),
    version,
    target
  ))

  return(invisible(target))
}

# internal helper
get_cli_executable <- function(tailwindcss = NULL) {
  if (is.null(tailwindcss)) {
    if (Sys.info()[["sysname"]] == "Windows") {
      tailwindcss <- "tailwindcss.exe"
    } else {
      tailwindcss <- "./tailwindcss"
    }
  }
  return(tailwindcss)
}


#' Checks if 'TailwindCSS' CLI is installed
#'
#' To install the CLI of 'TailwindCSS', please follow the instructions of
#' ['TailwindCSS' releases](https://github.com/tailwindlabs/tailwindcss/releases).
#' Make sure that you either provide the direction to the executable as the
#' first argument to this function or put it in a folder on your PATH variable.
#'
#' @param tailwindcss name and path to the executable
#' @param verbose report version number etc
#'
#' @return TRUE/FALSE if the CLI is installed
#' @export
#'
#' @examples
#' if (interactive()) {
#'   is_tailwindcss_installed()
#' }
is_tailwindcss_installed <- function(tailwindcss = NULL, verbose = FALSE) {
  tailwindcss <- get_cli_executable(tailwindcss)

  cmd <- paste(tailwindcss, "-h")
  r <- try(system(cmd, intern = TRUE), silent = TRUE)

  if (inherits(r, "try-error") || length(r) <= 2) {
    if (verbose) {
      warning(paste(
        "Could not find CLI tailwindcss.",
        "Please follow install instructions and put it in your PATH or supply the path to this function",
        "Download: https://github.com/tailwindlabs/tailwindcss/releases",
        attr(r, "condition"),
        sep = "\n"
      ))
    }
    return(FALSE)
  }

  version <- gsub("tailwindcss +", "", r[[2]])
  if (verbose) {
    cat(sprintf("Found tailwindcss version %s\n", version))
  }
  return(TRUE)
}


#' Starts the 'TailwindCSS' CLI
#'
#' See [v4 docs](https://tailwindcss.com/docs/installation/play-cdn)
#' or [v3 docs](https://tailwindcss.com/blog/standalfone-cli).
#'
#' @param infile the 'TailwindCSS' file (eg containing the `@tailwind` directives). Relative to basedir
#' @param outfile the target css file, where tailwind will write the css to.
#'   Relative to basedir
#' @param version Which version of tailwind to use. Default is now v4.
#' @param config the path to the tailwind.config.js file. With v4, the config file is not needed and can instead be specified in the `infile` css.
#' With v3, the default is tailwind.config.js in the root diretory.
#'
#' @param watch if the files should be continuously monitored (versus only
#'   compile the css once), default is False
#' @param tailwindcss name and path to the executable
#' @param verbose print information
#' @param minify if the code should be minified, default is FALSE
#' @param content content paths to remove unused classes, default is current dir
#'
#' @return the outfile invisibly
#' @export
#'
#' @seealso [install_tailwindcss_cli]
#' @examples
#' if (interactive()) {
#'   temp <- tempdir()
#'   owd <- setwd(temp)
#'
#'   infile <- "custom.css"
#'   writeLines("@tailwind base;", infile)
#'   outfile <- "out.css"
#'
#'   # file.copy(system.file("examples", "01-Old_Faithful", "app.R", package = "shiny.tailwind"),
#'   #           "app.R", overwrite = TRUE)
#'
#'   # write a mini shiny UI
#'   writeLines("
#'     library(shiny)
#'     div(class = \"page-div\",
#'         div(class = \"w-full text-center py-12\",
#'             h1(\"Hello World\")
#'         )
#'     )", "app.R")
#'
#'   tailwindcss <- NULL # can be set to the executable file
#'   compile_tailwindcss(infile, outfile, tailwindcss = tailwindcss)
#'   cat(paste(readLines(outfile)[1:20], collapse = "\n"))
#'
#'   setwd(owd)
#' }
compile_tailwindcss <- function(
  infile,
  outfile,
  version = 4,
  config = "tailwind.config.js",
  watch = FALSE,
  minify = FALSE,
  content = ".",
  tailwindcss = NULL,
  verbose = FALSE
) {
  stopifnot(length(infile) == 1)
  stopifnot(length(outfile) == 1)

  if (!is_tailwindcss_installed(tailwindcss = tailwindcss)) {
    stop("Could not find an installation of tailwindcss!")
  }
  tailwindcss <- get_cli_executable(tailwindcss)

  # Create config if there is none
  if (!file.exists(config) && version == 3) {
    cat(sprintf(
      "Could not find %s, copying default from shiny.tailwind\n",
      config
    ))

    file.copy(
      system.file("default-tailwind.config.js", package = "shiny.tailwind"),
      config
    )
  }

  if (version == 3) {
    cmd <- paste(
      tailwindcss,
      "--config",
      config,
      "--input",
      infile,
      "--output",
      outfile,
      if (watch) "--watch",
      if (minify) "--minify"
    )
  } else {
    cmd <- paste(
      tailwindcss,
      "--input",
      infile,
      "--output",
      outfile,
      if (watch) "--watch",
      if (minify) "--minify"
    )
  }
  if (verbose) cat(paste0("Running tailwindcss CLI command:\n  ", cmd))

  a <- try(system(cmd, intern = TRUE), silent = TRUE)
  if (inherits(a, "try-error")) {
    cat(gsub("\\\\r\\\\n", "\n", a))
    stop("Could not execute tailwindcss CLI with error\n", a)
  }

  return()
}
kylebutts/shiny.tailwind documentation built on April 17, 2025, 5:21 p.m.