diff --git a/.lintr b/.lintr index 75c558e..017041b 100644 --- a/.lintr +++ b/.lintr @@ -3,7 +3,7 @@ linters: linters_with_defaults( implicit_integer_linter(), indentation_linter(indent = 4L), object_name_linter(styles = c("snake_case", "symbols"), regexes = character()), - object_name_linter = NULL + object_usage_linter = NULL ) exclusions: list("man/", "inst/", "src/", ".vscode/", ".Rproj.user/", "R/import-standalone-obj-type.R", "R/import-standalone-types-check.R") encoding: "UTF-8" diff --git a/DESCRIPTION b/DESCRIPTION index cc572b9..750fc7b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,10 +1,10 @@ Package: hotwater -Title: autoreload plumber APIs +Title: Live Reload for Plumber APIs Version: 0.0.0.9002 Authors@R: person("Elian", "Thiele-Evans", , "elianhte@gmail.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0001-8008-3165")) -Description: What the package does (one paragraph). +Description: Enhances development for plumber APIs by enabling live reloading. Monitors API files for changes and automatically refreshes the server and connected web clients, allowing for faster API iteration. License: GPL (>= 3) URL: https://github.com/ElianHugh/hotwater BugReports: https://github.com/ElianHugh/hotwater/issues @@ -19,9 +19,7 @@ Imports: Suggests: box, docopt, - remotes, testthat (>= 3.0.0), - xml2, withr Config/testthat/edition: 3 Encoding: UTF-8 diff --git a/R/middleware.R b/R/middleware.R index 7f5796c..6ee2870 100644 --- a/R/middleware.R +++ b/R/middleware.R @@ -50,7 +50,11 @@ publish_browser_reload <- function(engine) { is_plumber_running <- function(engine) { tryCatch( expr = { - url <- sprintf("localhost:%s/__hotwater__", engine$config$port) + url <- sprintf( + "%s:%s/__hotwater__", + engine$config$host, + engine$config$port + ) res <- httr2::request(url) |> httr2::req_perform() |> httr2::resp_status() diff --git a/R/run.R b/R/run.R index 16c2065..4fabefe 100644 --- a/R/run.R +++ b/R/run.R @@ -1,25 +1,57 @@ -#' Start hotwater engine +#' Start a hotwater engine #' #' @description -#' Start a hotwater engine, which launches a plumber API that is restarted whenever -#' the plumber API's folder is modified. #' -#' Extra directories can be specified to refresh the API when directories other than the plumber folder are modified. +#' Start the hotwater engine, launching a plumber API +#' that is restarted whenever a file in the plumber API's folder is modified. #' -#' If a plumber endpoint returns an html response, when hotwater refreshes the API, hotwater will also order -#' a refresh of any webpage that is using the API. +#' Extra directories can be specified to refresh the API when +#' directories other than the plumber folder are modified. #' -#' @param path path to plumber file -#' @param dirs extra directories to watch -#' @param port port to launch API on, defaults to `httpuv::randomPort()` -#' @param host host to launch API on, defaults to "127.0.0.1" -#' @param ignore vector of files or file extensions to ignore (globs) +#' If a plumber endpoint returns an HTML response, when hotwater +#' refreshes the API, \{hotwater\} will also order a refresh of any +#' webpage that is using the API. +#' +#' @details +#' +#' To refresh the browser, a postserialize [plumber::pr_hook] is used to +#' inject a websocket into the HTML client that listens for the +#' plumber server refresh. +#' +#' @param path path to plumber API file. +#' +#' @param dirs (optional) a character vector of extra directories +#' to watch for file changes. Paths are resolved from the current working +#' directory, not the directory of the plumber API file. +#' +#' @param port \[default [httpuv::randomPort()]] port to launch API on. +#' +#' port can either be set explicitly, or it defaults to the +#' `plumber.port` option. If the plumber option is undefined, the fallback +#' value of [httpuv::randomPort()] is used. +#' +#' @param host \[default "127.0.0.1"] host to launch API on. +#' +#' host can either be set explicitly, or it defaults to the +#' `plumber.host` option. If the plumber option is undefined, the fallback +#' value of "127.0.0.1" is used. +#' +#' @param ignore \[default `c("*.sqlite", "*.git*")`] vector of file globs +#' to ignore. +#' +#' @seealso [plumber::options_plumber], +#' [plumber::get_option_or_env], [plumber::serializer_html] #' #' @examples #' if (interactive()) { -#' hotwater::run(system.file("examples", "plumber.R", package = "hotwater")) +#' # start a hotwater session on port 9999 +#' hotwater::run( +#' path = system.file("examples", "plumber.R", package = "hotwater"), +#' port = 9999L +#' ) #' } #' +#' @return NULL #' @export run <- function(path, dirs = NULL, port = NULL, host = NULL, ignore = NULL) { config <- new_config( diff --git a/R/script.R b/R/script.R index 46eff55..9f0d742 100644 --- a/R/script.R +++ b/R/script.R @@ -1,3 +1,27 @@ +#' @title Run hotwater from the command line +#' +#' @description +#' Following [hotwater::install_hotwater()], the `hotwater` command can be used +#' to run a hotwater engine straight from the terminal. See +#' [hotwater::run()] for further details on default values. +#' +#' `hotwater -v` will provide the current version of hotwater. +#' `hotwater -h` will provide help text. +#' +#' @param -f plumber file +#' @param -d extra directories +#' @param -p plumber port +#' @param -h show help +#' @param --host plumber host +#' @seealso [hotwater::run()] +#' @examples +#' # ```sh +#' # hotwater -f path/to/app.R -p 9999 +#' # ``` +#' @rdname cli +#' @name cli +NULL + common_install_paths <- list( unix = c( "~/.local/bin/", @@ -9,8 +33,21 @@ common_install_paths <- list( windows = c() # does windows even work with this? ) -#' WORK IN PROGRESS -#' @param install_folder folder (in PATH) to install hotwater +#' Install global hotwater script +#' +#' If hotwater is installed, users may run `hotwater` from the command line +#' rather than from an R terminal. +#' +#' @param install_folder \[default "~/.local/bin/"] folder to install hotwater +#' script into. To run as expected, make sure that the folder supplied is on your +#' `PATH` envar. +#' @seealso [hotwater::uninstall_hotwater] +#' @examples +#' if (interactive()) { +#' hotwater::install_hotwater() +#' } +#' @return NULL +#' #' @export install_hotwater <- function(install_folder = "~/.local/bin/") { p <- file.path(install_folder, "hotwater") @@ -31,8 +68,16 @@ install_hotwater <- function(install_folder = "~/.local/bin/") { } } -#' WORK IN PROGRESS -#' @param install_folder folder (in PATH) to uninstall hotwater +#' Uninstall global hotwater script +#' +#' @param install_folder \[default "~/.local/bin/"] folder to uninstall hotwater +#' from. +#' @examples +#' if (interactive()) { +#' hotwater::uninstall_hotwater() +#' } +#' @seealso [hotwater::install_hotwater] +#' @return NULL #' @export uninstall_hotwater <- function(install_folder = "~/.local/bin/") { p <- file.path(install_folder, "hotwater") @@ -50,7 +95,7 @@ uninstall_hotwater <- function(install_folder = "~/.local/bin/") { #' Check suggested packages for CLI usage #' -#' The {docopt} and {remotes} packages are required to run hotwater from the command line. +#' The {docopt} package is required to run hotwater from the command line. #' #' @noRd check_suggests <- function() { @@ -68,13 +113,11 @@ check_suggests <- function() { } } -#' Run hotwater as a bash script -#' @noRd run_cli <- function() { doc <- "hotwater Usage: - hotwater --file=FILE [--dirs=DIRS] [--port=PORT] [--host=SERVER] + hotwater --file=FILE [--dirs=DIRS] [--port=PORT] [--host=HOST] hotwater -h | --help hotwater -v | --version @@ -84,7 +127,7 @@ run_cli <- function() { -f FILE --file=FILE plumber path (required) -d DIRS --dirs=DIRS extra directories -p PORT --port=PORT plumber port - -s SERVER --server=SERVER plumber host + --host=HOST plumber host " args <- docopt::docopt( @@ -96,6 +139,6 @@ run_cli <- function() { path = args$file, dirs = args$dirs, port = if (is.null(args$port)) NULL else as.numeric(args$port), - host = args$server + host = args$host ) } diff --git a/R/utils.R b/R/utils.R index caf79c7..ad6b791 100644 --- a/R/utils.R +++ b/R/utils.R @@ -4,6 +4,6 @@ if (is.null(x)) y else x } -`%|NA|%` <- function(x, y) { +`%|NA|%` <- function(x, y) { # nolint: object_name_linter. if (is.na(x)) y else x -} \ No newline at end of file +} diff --git a/README.md b/README.md index 7b6f377..d099347 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -# hotwater +# 🌡️💧 hotwater [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) +[![hotwater status badge](https://elianhugh.r-universe.dev/badges/hotwater)](https://elianhugh.r-universe.dev/hotwater) [![Codecov test coverage](https://codecov.io/gh/ElianHugh/hotwater/branch/main/graph/badge.svg)](https://app.codecov.io/gh/ElianHugh/hotwater?branch=main) [![R-CMD-check](https://github.com/ElianHugh/hotwater/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/ElianHugh/hotwater/actions/workflows/R-CMD-check.yaml) -- for plumber development - autoreload for plumber -- also auto-refreshes the browser when a change is made +- auto-refresh the browser when a change is made - run from the commandline with the `/exec/hotwater` bash script ## Installation diff --git a/exec/hotwater b/exec/hotwater index 0ce2275..5e6eaa1 100644 --- a/exec/hotwater +++ b/exec/hotwater @@ -2,7 +2,7 @@ if (!requireNamespace("hotwater", quietly = TRUE)) { cli::cli_inform("Bootstrapping hotwater...") - remotes::install_github("ElianHugh/hotwater") + utils::install.packages("hotwater", repos = "https://elianhugh.r-universe.dev") } hotwater:::check_suggests() diff --git a/man/cli.Rd b/man/cli.Rd new file mode 100644 index 0000000..89bea8c --- /dev/null +++ b/man/cli.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/script.R +\name{cli} +\alias{cli} +\title{Run hotwater from the command line} +\arguments{ +\item{-f}{plumber file} + +\item{-d}{extra directories} + +\item{-p}{plumber port} + +\item{-h}{show help} + +\item{--host}{plumber host} +} +\description{ +Following \code{\link[=install_hotwater]{install_hotwater()}}, the \code{hotwater} command can be used +to run a hotwater engine straight from the terminal. See +\code{\link[=run]{run()}} for further details on default values. + +\code{hotwater -v} will provide the current version of hotwater. +\code{hotwater -h} will provide help text. +} +\examples{ +# ```sh +# hotwater -f path/to/app.R -p 9999 +# ``` +} +\seealso{ +\code{\link[=run]{run()}} +} diff --git a/man/install_hotwater.Rd b/man/install_hotwater.Rd index 169ca29..11c3194 100644 --- a/man/install_hotwater.Rd +++ b/man/install_hotwater.Rd @@ -2,13 +2,24 @@ % Please edit documentation in R/script.R \name{install_hotwater} \alias{install_hotwater} -\title{WORK IN PROGRESS} +\title{Install global hotwater script} \usage{ install_hotwater(install_folder = "~/.local/bin/") } \arguments{ -\item{install_folder}{folder (in PATH) to install hotwater} +\item{install_folder}{[default "~/.local/bin/"] folder to install hotwater +script into. To run as expected, make sure that the folder supplied is on your +\code{PATH} envar.} } \description{ -WORK IN PROGRESS +If hotwater is installed, users may run \code{hotwater} from the command line +rather than from an R terminal. +} +\examples{ +if (interactive()) { + hotwater::install_hotwater() +} +} +\seealso{ +\link{uninstall_hotwater} } diff --git a/man/run.Rd b/man/run.Rd index 3e7c973..38b05d5 100644 --- a/man/run.Rd +++ b/man/run.Rd @@ -2,33 +2,59 @@ % Please edit documentation in R/run.R \name{run} \alias{run} -\title{Start hotwater engine} +\title{Start a hotwater engine} \usage{ run(path, dirs = NULL, port = NULL, host = NULL, ignore = NULL) } \arguments{ -\item{path}{path to plumber file} +\item{path}{path to plumber API file.} -\item{dirs}{extra directories to watch} +\item{dirs}{(optional) a character vector of extra directories +to watch for file changes. Paths are resolved from the current working +directory, not the directory of the plumber API file.} -\item{port}{port to launch API on, defaults to \code{httpuv::randomPort()}} +\item{port}{[default \code{\link[httpuv:randomPort]{httpuv::randomPort()}}] port to launch API on. -\item{host}{host to launch API on, defaults to "127.0.0.1"} +port can either be set explicitly, or it defaults to the +\code{plumber.port} option. If the plumber option is undefined, the fallback +value of \code{\link[httpuv:randomPort]{httpuv::randomPort()}} is used.} -\item{ignore}{vector of files or file extensions to ignore (globs)} +\item{host}{[default "127.0.0.1"] host to launch API on. + +host can either be set explicitly, or it defaults to the +\code{plumber.host} option. If the plumber option is undefined, the fallback +value of "127.0.0.1" is used.} + +\item{ignore}{[default \code{c("*.sqlite", "*.git*")}] vector of file globs +to ignore.} } \description{ -Start a hotwater engine, which launches a plumber API that is restarted whenever -the plumber API's folder is modified. +Start the hotwater engine, launching a plumber API +that is restarted whenever a file in the plumber API's folder is modified. -Extra directories can be specified to refresh the API when directories other than the plumber folder are modified. +Extra directories can be specified to refresh the API when +directories other than the plumber folder are modified. -If a plumber endpoint returns an html response, when hotwater refreshes the API, hotwater will also order -a refresh of any webpage that is using the API. +If a plumber endpoint returns an HTML response, when hotwater +refreshes the API, \{hotwater\} will also order a refresh of any +webpage that is using the API. +} +\details{ +To refresh the browser, a postserialize \link[plumber:pr_hook]{plumber::pr_hook} is used to +inject a websocket into the HTML client that listens for the +plumber server refresh. } \examples{ if (interactive()) { - hotwater::run(system.file("examples", "plumber.R", package = "hotwater")) + # start a hotwater session on port 9999 + hotwater::run( + path = system.file("examples", "plumber.R", package = "hotwater"), + port = 9999L + ) } } +\seealso{ +\link[plumber:options_plumber]{plumber::options_plumber}, +\link[plumber:options_plumber]{plumber::get_option_or_env}, \link[plumber:serializers]{plumber::serializer_html} +} diff --git a/man/uninstall_hotwater.Rd b/man/uninstall_hotwater.Rd index 6994000..4f8cf8a 100644 --- a/man/uninstall_hotwater.Rd +++ b/man/uninstall_hotwater.Rd @@ -2,13 +2,22 @@ % Please edit documentation in R/script.R \name{uninstall_hotwater} \alias{uninstall_hotwater} -\title{WORK IN PROGRESS} +\title{Uninstall global hotwater script} \usage{ uninstall_hotwater(install_folder = "~/.local/bin/") } \arguments{ -\item{install_folder}{folder (in PATH) to uninstall hotwater} +\item{install_folder}{[default "~/.local/bin/"] folder to uninstall hotwater +from.} } \description{ -WORK IN PROGRESS +Uninstall global hotwater script +} +\examples{ +if (interactive()) { + hotwater::uninstall_hotwater() +} +} +\seealso{ +\link{install_hotwater} } diff --git a/tests/testthat/test-middleware.R b/tests/testthat/test-middleware.R index 0a04086..ad00211 100644 --- a/tests/testthat/test-middleware.R +++ b/tests/testthat/test-middleware.R @@ -74,9 +74,20 @@ test_that("is_plumber_running works", { test_that("autoreloader is attached", { engine <- new_test_engine() new_runner(engine) - resp <- httr2::request(sprintf("localhost:%s", engine$config$port)) |> - httr2::req_perform() |> - httr2::resp_body_html() - expect_true(grepl(resp, pattern = "