Skip to content

Revise-compatible hot-reloading middleware for web development in Julia

License

Notifications You must be signed in to change notification settings

MichaelHatherly/ReloadableMiddleware.jl

Repository files navigation

ReloadableMiddleware.jl

Revise-compatible hot-reloading middleware for web development in Julia

This package provides a collection of utilities and HTTP middleware that improves developer experience while working on server-based Julia projects.

Exported Modules

The following modules are exported and make up the public API of this package.

Server

Provides the dev and prod function. These start up a "development" and "production" server respectively. The dev server includes features such as:

dev features

Automatic Page Updates

Automatic page updates on file changes without refreshing browser tabs. This will watch all source files for changes and push the updated HTML to the browser via SSE (server sent events). New HTML content is swapped with the current HTML with idiomorph.

This feature integrates with Revise and a router auto-reloader, which allows for addition and removal of route definitions in the server without the need to restart it. The usual limitations of Revise apply to this feature, ie. no struct re-definitions, etc.

Automatic Browser Tab Opening

When starting up a dev server a browser tab is opened pointing to the / route. On macOS if using a Chromium-based browser then if there is alread a tab open pointing at this server then it is reused. This uses the same approach used by Vite.

Source Code Lookup

"DOM-to-Source" lookup, via HypertextTemplates.jl. When using this package for template rendering source information for all nodes in the rendered web page are stored and can be used to jump to the source location in your default editor by selecting a part of the webpage, typically some text, and pressing Ctrl+2. Pressing Ctrl+1 will jump to the root @render macro call that generated the element, which can be useful if the current state of a page consists of HTML generated by different routes. This feature requires Revise to be imported for it to function.

/docs/ route

All documentation for defined routes that make up the application is available at the /docs/ route. This change be changed via the docs keyword provided by dev. This provides a summary overview and details view for each route, which includes any attached docstrings, path, query, and body types, along with source code links that will open your default editor to the source of the route.

/errors/ route

Any errors that are thrown during development cause a separate browser tab to open with an interactive view of the stacktrace and error message. Source links in the stacktrace are clickable and open your default editor at that location.

The /errors/ root route itself lists all errors that the application has encountered while running, and allows you to navigate back to previous errors.

The name of this route change be changed if it conflicts with your application by changing the errors keyword in the dev call.

prod features

The prod function removes all the above features of dev. Aside from that the interface is identical. Ensure that Revise is not loaded when running a server via prod in production since you may leak source location information from rendered templates.

Router

This module provides the route macros @GET, @POST, @PUT, @PATCH, and @DELETE matching their HTTP verbs, along with @STREAM and @WEBSOCKET macros. These are used to define HTTP route handlers within the enclosing module. They all follow the pattern below:

module Routes

using ReloadableMiddleware.Router

# Handler functions must be unnamed function definitions.
@GET "/" function (req)
    # Anything can be returned from the handler. It will be converted
    # to an `HTTP.Response` automatically. See the `Responses` section
    # below for details.
    return "page content..."
end

# Use the same `{}` syntax as `HTTP`. Additionally include a `path` keyword
# that declares the type of each path parameter. `StructTypes` is used for
# the deserialization of these parameters.
@GET "/page/{id}" function (req; path::@NamedTuple{id::Int})
    @show path.id
    # ...
end

# Post requests specify their typed body contents as below. This is
# expected to be urlencoded `Content-Type`.
@POST "/form" function (req; body::@NamedTuple{a::String,b::Base.UUID})
    @show body.a, body.b
    # ...
end

# Query parameters are specified using the `query` keyword. These are
# urlencoded values, and are deserialized, like the previous examples
# using `StructTypes`.
@GET "/search" function (req; query::@NamedTuple{q::String})
    @show query.q
    # ...
end

# JSON data can be deserialized using the `JSON` type. Anything that
# the `JSON3` package handles can be handled with this type. `allow_inf`
# is set to `true` for deserialization.
@POST "/api/v1/info" function (req; body::JSON{Vector{Float64}})
    @show vec = body.json
    # ...
end

# Multipart form data can be deserialized using the `Multipart` and `RawFile`
# type as below.
@POST "/file-upload" function (req; body::Multipart{@NamedTuple{file::RawFile}})
    @show body.multipart.file body.multipart.data
    # ...
end

# Multipart form data can be deserialized using the `Multipart` and `File`
# type as below. `.data` will contain a `Vector{Int}` rather than raw bytes.
# The `Content-Type` of the part denotes how it will be deserialized, either
# as JSON or urlencoded.
@POST "/file-upload-typed" function (req; body::Multipart{@NamedTuple{file::File{Vector{Int}}}})
    @show body.multipart.file body.multipart.data
    # ...
end

Scoped Routes

The @prefix and @middleware macros allow for defining "scoped" routes. This provides a way to group a set of routes under a common path prefix and a scoped set of middleware that should be applied in addition to the Server-level middleware.

These macros work on the module-level, ie. only a single usage per-module is allowed and affects all routes defined within the module. Use different modules to separate different sets of routes, for example a Routes.API module to define a JSON API that lives under a /api/v1 route prefix.

module Routes

@GET "/" function (req)
    # The actual `/` handler.
end

module API

@prefix "/api/v1"

@middleware function (handler)
    function (req)
        # Run custom middleware code here. Only # runs for routes under `/api/v1`.
        return handler(req)
    end
end

@GET "/" function (req)
    # Handles `/api/v1`, not `/`.
end

end

end

Validated router paths

You can construct valid paths to any defined route by calling the route handler function with no positional arguments and either/both of the path and query keyword arguments. If called with invalid values it will fail at runtime to construct the path. This only works for named route handler functions.

@GET "/users/{id}" function user_details(req; path::@NamedTuple{id::Int})
    # ...
end

# ...

user_details(; path = (; id = 1)) == "/users/1"
user_details(; path = (; id = "id")) # Throws an error.

Responses

This module is responsible for serializing returned values from route handlers into valid HTTP.Responses. Any value returned from a handler is supported. Strings are content-sniffed using HTTP.sniff to determine Content-Type. Base.Docs.HTML and Base.Docs.Text are set to text/html and text/plain respectively. Simple values such as numbers, symbols, and characters are sent as text/plain. Other values are serialized as application/json using the JSON3 package, using the allow_inf = true keyword.

The module exports a response helper function that allows for declaring the intended serialization mime type, along with other options such as attachment and filename which effect the Content-Disposition header.

Extensions

This module offers interfaces that enhance features and behaviors of the package but that rely on external Julia packages that are not direct dependencies of ReloadableMiddleware.

bonito_middleware

This middleware function provides integration with Bonito.jl such that users can embed Bonito.Apps into rendered pages without having to manually setup WebSocket connections and handle closing unused sessions. Just add

middleware = [bonito_middleware(), ...]

to your Server.prod and Server.dev calls that launch your server. Then embed Apps into the rendered HTML from any routes and they will maintain an open WebSocket with the backend. This is useful for such things as interactive WGLMakie plots.

Vary: * Header

Note that when a user nagivates away from a page that contains an active Bonito.App the connection will be closed and the server will terminate the Bonito.Session and all associated assets that were being served for it to function. If a user then decides to navigate back to that page with their browser nagivation they may not get a newly requested page content, which means there is no running Bonito session to connect to. This is intentional. Use a Vary: * header for any pages that contain such dynamic content so that the browser is forced to do a full refetch of the page content. This will start up a new Bonito connection and session.

About

Revise-compatible hot-reloading middleware for web development in Julia

Topics

Resources

License

Stars

Watchers

Forks

Contributors 4

  •  
  •  
  •  
  •