Skip to content

Commit

Permalink
revert to HTTP 0.9 but with #142 #144
Browse files Browse the repository at this point in the history
  • Loading branch information
tlienart committed Aug 14, 2022
1 parent abcd6ad commit b28e291
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 73 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "LiveServer"
uuid = "16fef848-5104-11e9-1b77-fb7a48bbb589"
authors = ["Jonas Asprion <jonas.asprion@gmx.ch", "Thibaut Lienart <tlienart@me.com>"]
version = "0.8.3"
version = "0.10.0"

[deps]
Crayons = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
Expand Down
161 changes: 97 additions & 64 deletions src/server.jl
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ Return the filesystem path corresponding to a requested path, or an empty
String if the file was not found.
"""
function get_fs_path(req_path::AbstractString)::String
uri = HTTP.URI(req_path)
# first element after the split is **always** "/" --> 2:end
uri = HTTP.URI(req_path)
r_parts = HTTP.URIs.unescapeuri.(split(lstrip(uri.path, '/'), '/'))
fs_path = joinpath(r_parts...)

Expand All @@ -95,7 +94,8 @@ function get_fs_path(req_path::AbstractString)::String
isfile(tmp) && return tmp

# content of the dir will be shown
isdir(fs_path) && return fs_path
# we ensure there's a slash at the end (see issue #135)
isdir(fs_path) && return joinpath(fs_path, "")

# 404 will be shown
return ""
Expand All @@ -109,7 +109,7 @@ Append `/` to the path part of `url`; i.e., transform `a/b` to `a/b/` and `/a/b?
"""
function append_slash(url_str::AbstractString)
uri = HTTP.URI(url_str)
return string(endswith(uri.path, "/") ? uri : merge(uri; path = uri.path * "/"))
return string(endswith(uri.path, "/") ? uri : HTTP.merge(uri; path = uri.path * "/"))
end

"""
Expand Down Expand Up @@ -140,16 +140,18 @@ function get_dir_list(dir::AbstractString)
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/spcss">
<title>Directory listing</title>
<style>
a {text-decoration: none;}
a {text-decoration: none;}
</style>
</head>
<body>
<h1 style='margin-top: 1em;'>
Directory listing
</h1>
<h3>
<a href="/" alt="root">🏠</a> <a href="/$(dirname(dir))" alt="parent dir">⬆️</a> &nbsp; path: <code style='color:gray;'>$(sdir)</code>
</h2>
<a href="/" alt="root">🏠</a>
<a href="/$(dirname(dir))" alt="parent dir">⬆️</a>
&nbsp; path: <code style='color:gray;'>$(sdir)</code>
</h3>
<hr>
<ul>
Expand All @@ -160,19 +162,21 @@ function get_dir_list(dir::AbstractString)
list_dirs = [d for d in list if d list_files]

for fname in list_files
link = lstrip_cdir(fname)
name = splitdir(fname)[end]
post = ifelse(islink(fname), " @", "")
link = lstrip_cdir(fname)
name = splitdir(fname)[end]
post = ifelse(islink(fname), " @", "")
write(io, """
<li><a href="/$(link)">$(name)$(post)</a></li>
"""
)
end
for fdir in list_dirs
link = lstrip_cdir(fdir)
name = splitdir(fdir)[end]
pre = "📂 "
post = ifelse(islink(fdir), " @", "")
link = lstrip_cdir(fdir)
# ensure ends with slash, see #135
link *= ifelse(endswith(link, "/"), "", "/")
name = splitdir(fdir)[end]
pre = "📂 "
post = ifelse(islink(fdir), " @", "")
write(io, """
<li><a href="/$(link)">$(pre)$(name)$(post)</a></li>
"""
Expand All @@ -197,14 +201,15 @@ to be watched can be added, and a request (e.g. a path entered in a tab of the
browser), and converts it to the appropriate file system path.
The cases are as follows:
1. the path corresponds exactly to a file. If it's a html-like file,
LiveServer will try injecting the reloading `<script>` (see file
`client.html`) at the end, just before the `</body>` tag.
2. the path corresponds to a directory in which there is an `index.html`,
same action as (1) assuming the `index.html` is implicit.
3. the path corresponds to a directory in which there is not an `index.html`,
list the directory contents.
4. not (1,2,3), a 404 is served.
1. FILE: the path corresponds exactly to a file. If it's a html-like file,
LiveServer will try injecting the reloading `<script>` (see file
`client.html`) at the end, just before the `</body>` tag. Otherwise
we let the browser attempt to show it (e.g. if it's an image).
2. WEB-DIRECTORY: the path corresponds to a directory in which there is an
`index.html`, same action as (1) assuming the `index.html` is implicit.
3. PLAIN-DIRECTORY: the path corresponds to a directory in which there is not
an `index.html`, list the directory contents.
4. 404: not (1,2,3), a 404 is served.
All files served are added to the file watcher, which is responsible to check
whether they're already watched or not. Finally the file is served via a 200
Expand All @@ -221,9 +226,8 @@ function serve_file(
fs_path = get_fs_path(req.target)

# if get_fs_path returns an empty string, there's two cases:
# 1. the path is a directory without an `index.html` --> list dir
# 2. otherwise serve a 404 (see if there's a dedicated 404 path,
# otherwise just use a basic one).
# 1. [CASE 3] the path is a directory without an `index.html` --> list dir
# 2. [CASE 4] otherwise serve a 404 (see if there's a dedicated 404 path,
if isempty(fs_path)

if req.target == "/"
Expand Down Expand Up @@ -251,14 +255,15 @@ function serve_file(
end
end

# [CASE 2]
if isdir(fs_path)
index_page = get_dir_list(fs_path)
return HTTP.Response(200, index_page)
end

# In what follows, fs_path points to a file
# --> html-like: try to inject reload-script
# --> other: just get the browser to show it
# --> [CASE 1a] html-like: try to inject reload-script
# --> [CASE 1b] other: just get the browser to show it
#
ext = lstrip(last(splitext(fs_path)), '.') |> string
content = read(fs_path, String)
Expand Down Expand Up @@ -391,59 +396,87 @@ end


"""
serve(filewatcher; host="127.0.0.1", port=8000, dir="", verbose=false, coreloopfun=(c,fw)->nothing, inject_browser_reload_script::Bool = true, launch_browser::Bool = false, allow_cors::Bool = false)
serve(filewatcher; ...)
Main function to start a server at `http://host:port` and render what is in the current
directory. (See also [`example`](@ref) for an example folder).
# Arguments
- `filewatcher` is a file watcher implementing the API described for [`SimpleWatcher`](@ref) (which also is the default) and messaging the viewers (via WebSockets) upon detecting file changes.
- `port` is an integer between 8000 (default) and 9000.
- `dir` specifies where to launch the server if not the current working directory.
- `verbose` is a boolean switch to make the server print information about file changes and connections.
- `coreloopfun` specifies a function which can be run every 0.1 second while the liveserver is going; it takes two arguments: the cycle counter and the filewatcher. By default the coreloop does nothing.
- `launch_browser=false` specifies whether to launch the ambient browser at the localhost URL or not.
- `allow_cors::Bool=false` will allow cross origin (CORS) requests to access the server via the "Access-Control-Allow-Origin" header.
- `preprocess_request=identity`: specifies a function which can transform a request before a response is returned; its only argument is the current request.
# Example
```julia
LiveServer.example()
serve(host="127.0.0.1", port=8080, dir="example", verbose=true, launch_browser=true)
```
You should then see the `index.html` page from the `example` folder being rendered. If you change the file, the browser will automatically reload the
page and show the changes.
"""
function serve(fw::FileWatcher=SimpleWatcher(file_changed_callback);
host::String="127.0.0.1", port::Int=8000, dir::AbstractString="", verbose::Bool=false,
coreloopfun::Function=(c, fw)->nothing,
preprocess_request=identity,
inject_browser_reload_script::Bool = true,
launch_browser::Bool = false,
allow_cors::Bool = false)

8000 port 9000 || throw(ArgumentError("The port must be between 8000 and 9000."))
setverbose(verbose)

if !isempty(dir)
isdir(dir) || throw(ArgumentError("The specified dir '$dir' is not recognised."))
CONTENT_DIR[] = dir
end
`filewatcher`: a file watcher implementing the API described for
[`SimpleWatcher`](@ref) (which also is the default) and
messaging the viewers (via WebSockets) upon detecting file
changes.
`port`: integer between 8000 (default) and 9000.
`dir`: string specifying where to launch the server if not the current
working directory.
`verbose`: boolean switch to make the server print information about file
changes and connections.
`coreloopfun`: function which can be run every 0.1 second while the
liveserver is running; it takes two arguments: the cycle
counter and the filewatcher. By default the coreloop does
nothing.
`launch_browser`: boolean specifying whether to launch the ambient browser
at the localhost or not (default: false).
`allow_cors`: boolean allowing cross origin (CORS) requests to access the
server via the "Access-Control-Allow-Origin" header.
`preprocess_request`: function specifying the transformation of a request
before it is returned; its only argument is the
current request.
# Example
```julia
LiveServer.example()
serve(host="127.0.0.1", port=8080, dir="example", launch_browser=true)
```
You should then see the `index.html` page from the `example` folder being
rendered. If you change the file, the browser will automatically reload the
page and show the changes.
"""
function serve(
fw::FileWatcher=SimpleWatcher(file_changed_callback);
# kwargs
host::String = "127.0.0.1",
port::Int = 8000,
dir::AbstractString = "",
verbose::Bool = false,
coreloopfun::Function = (c, fw)->nothing,
preprocess_request::Function = identity,
inject_browser_reload_script::Bool = true,
launch_browser::Bool = false,
allow_cors::Bool = false
)::Nothing

8000 port 9000 || throw(
ArgumentError("The port must be between 8000 and 9000.")
)
setverbose(verbose)

if !isempty(dir)
isdir(dir) || throw(
ArgumentError("The specified dir '$dir' is not recognised.")
)
CONTENT_DIR[] = dir
end

start(fw)

# make request handler
req_handler = HTTP.RequestHandlerFunction() do req
req = preprocess_request(req)
serve_file(fw, req; inject_browser_reload_script = inject_browser_reload_script, allow_cors = allow_cors)
serve_file(
fw, req;
inject_browser_reload_script = inject_browser_reload_script,
allow_cors = allow_cors
)
end

server = Sockets.listen(parse(IPAddr, host), port)
url = "http://$(host == string(Sockets.localhost) ? "localhost" : host):$port"
println("✓ LiveServer listening on $url/ ...\n (use CTRL+C to shut down)")
println(
"✓ LiveServer listening on $url/ ...\n (use CTRL+C to shut down)"
)
@async HTTP.listen(host, port;
server=server, readtimeout=0, reuse_limit=0) do http::HTTP.Stream
# reuse_limit=0 ensures that there won't be an error if killing and restarting the server.
Expand Down
16 changes: 12 additions & 4 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,18 @@ function servedocs_callback!(
path2makejl::AbstractString,
literate::Union{Nothing,String},
skip_dirs::Vector{String},
skip_files::Vector{String},
foldername::String,
buildfoldername::String)
# ignore things happening in the build folder (generated files etc)
startswith(fp, joinpath(foldername, buildfoldername)) && return nothing
# ignore things happening in any skip_dirs
# ignore files skip_dirs and skip_files
for dir in skip_dirs
startswith(fp, dir) && return nothing
end
startswith(fp, dir) && return nothing
end
for file in skip_files
fp == file && return nothing
end

# if the file that was changed is the `make.jl` file, assume that maybe
# new files have been generated and so refresh the vector of watched files
Expand Down Expand Up @@ -167,6 +171,7 @@ subfolder `docs`.
`skip_dir=joinpath("docs","src","examples")`.
* `skip_dirs=[]`: same as `skip_dir` but for a list of such dirs. Takes
precedence over `skip_dir`.
* `skip_files=[]`: a vector of files that should not trigger regeneration.
* `foldername="docs"`: specify a different path for the content.
* `buildfoldername="build"`: specify a different path for the build.
* `makejl="make.jl"`: path of the script generating the documentation relative
Expand All @@ -182,6 +187,7 @@ function servedocs(;
doc_env::Bool=false,
skip_dir::String="",
skip_dirs::Vector{String}=String[],
skip_files::Vector{String}=String[],
foldername::String="docs",
buildfoldername::String="build",
makejl::String="make.jl",
Expand All @@ -193,6 +199,8 @@ function servedocs(;
if isempty(skip_dirs) && !isempty(skip_dir)
skip_dirs = [skip_dir]
end
skip_dirs = abspath.(skip_dirs)
skip_files = abspath.(skip_files)

path2makejl = joinpath(foldername, makejl)

Expand All @@ -203,7 +211,7 @@ function servedocs(;
docwatcher,
fp -> servedocs_callback!(
docwatcher, fp, path2makejl,
literate, skip_dirs, foldername, buildfoldername
literate, skip_dirs, skip_files, foldername, buildfoldername
)
)

Expand Down
8 changes: 4 additions & 4 deletions test/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
include(abspath(makejl))
@test readmake() == 1

def = (nothing, String[], "docs", "build")
def = (nothing, String[], String[], "docs", "build")

# callback function
dw = LS.SimpleWatcher()
Expand Down Expand Up @@ -107,7 +107,7 @@ end
# callback function
dw = LS.SimpleWatcher()

LS.servedocs_callback!(dw, makejl, makejl, "", String[], "site", "build")
LS.servedocs_callback!(dw, makejl, makejl, "", String[], String[], "site", "build")

@test length(dw.watchedfiles) == 3
@test dw.watchedfiles[1].path == joinpath("site", "make.jl")
Expand All @@ -118,14 +118,14 @@ end

# let's now remove `index2.md`
rm(joinpath("site", "src", "index2.md"))
LS.servedocs_callback!(dw, makejl, makejl, "", String[], "site", "build")
LS.servedocs_callback!(dw, makejl, makejl, "", String[], String[], "site", "build")

# the file has been removed
@test length(dw.watchedfiles) == 2
@test readmake() == 3

# let's check there's an appropriate trigger for index
LS.servedocs_callback!(dw, joinpath("site", "src", "index.md"), makejl, "", String[], "site", "build")
LS.servedocs_callback!(dw, joinpath("site", "src", "index.md"), makejl, "", String[], String[], "site", "build")
@test length(dw.watchedfiles) == 2
@test readmake() == 4

Expand Down

2 comments on commit b28e291

@tlienart
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/66219

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.10.0 -m "<description of version>" b28e2917169a11c8bb2f3a04234f6f37b6f384a8
git push origin v0.10.0

Please sign in to comment.