Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/michaelhatherly/bundledwebresources.jl

Automatic local bundling of remote resources as relocatable Julia objects
https://github.com/michaelhatherly/bundledwebresources.jl

julia web-development

Last synced: about 1 month ago
JSON representation

Automatic local bundling of remote resources as relocatable Julia objects

Awesome Lists containing this project

README

        

# BundledWebResources.jl

_Automatic local bundling of remote resources as relocatable Julia objects_

A small Julia package to automate the process of bundling web resources from
remote URLs (usually CDNs) into embedded relocatable content that can then be
served from a single server rather than from multiple third-party sources.

## `Resource`s

```julia
module MyResources

using BundledWebResources

@register plotly_js() = Resource(
"https://cdn.jsdelivr.net/npm/[email protected]/dist/plotly.min.js";
sha256 = "bf56aa89e1d4df155b43b9192f2fd85dfb0e6279e05c025e6090d8503d004608",
)

# ...

end
```

You must provide a SHA256 hash of the expected content to ensure resource
integrity of the included files. Verify the validity of the hash before
including it in any deployments.

Above we define a `Resource` called `plotly_js` that provides a specific
version of the PlotlyJS library. All resources should be defined within a
module that is solely for housing resources. Each resource returning function
must be marked with the `@register` macro, which allows the package to detect
which functions provide resources when serving them to HTTP clients. Resource
functions marked with `@register` should always just define and return a
`Resource` or `LocalResource` and should not perform any dynamic computation to
define the resource parameters, since the function body is only evaluated once
at toplevel and then spliced into the function body. Use a transformer function
with a `LocalResource` if you need to perform dynamic content generation.

See the `?Resource` docstring for further details of the API it provides.

## `LocalResource`s

Local resources, such as artifacts generated by external tools such as JS or
CSS bundlers, can be made to participate in the same system but using the
`LocalResource` type. In combination with `RelocatableFolders` this allows for
easy deployment of built artifacts via system images.

```julia
using RelocatableFolders, BundledWebResources

const DIST_DIR = @path joinpath(@__DIR__, "dist")

@register function resource()
return LocalResource(DIST_DIR, "output.css")
end
```

See the `?LocalResource` docstring for further details of the API it provides.

### Resource transformer functions

The `LocalResource` type accepts a third optional argument that is a function
`(directory, file) -> content` that can be used to generate the content of the
resource at request time (cached in production use). This can be used to
generate resources from other resources, such as TypeScript to JavaScript conversion
using the experimental `bun_build` function.

```julia
@register function bundled_resource()
return LocalResource(
DATA_DIR,
"bundled.js",
BundledWebResources.bun_build("bundled.ts"),
)
end
```

Above `bun_build` returns a closure that will convert `bundled.ts` to `bundled.js`
using the `bun build` command.

You can use this optional transformer argument to do any kind of custom
processing of resources you want, even generating completely synthetic resources
from `String` content.

```julia
function fake_resource_content(_dir, _file)
return """
console.log("Hello, world!");
"""
end

@register function fake_resource()
return LocalResource(
DATA_DIR,
"fake.js",
fake_resource_content,
)
end
```

Note that these resource generating functions are `Revise`-aware and will
rebuild the resource content when the `String` content changes if you have
`Revise` loaded and browser hot-reloads enabled using `ReloadableMiddleware`.

## `@ResourceEndpoint`

The main use case of these resources is serving them to clients via an HTTP
server. The `@ResourceEndpoint` is used to serve bundled resources to clients.
When `Revise` is loaded these resources will "live updated", otherwise they'll
remain as static content in production builds that don't include `Revise`.

```julia
module MyBundledResources

using BundledWebResources

# ...

end

using HTTP, BundledWebResources

function static_resources(req::HTTP.Request)
return @ResourceEndpoint(MyBundledResources, req)
end

router = HTTP.Router()

HTTP.register!(router, "GET", "/static/**", static_resources)

HTTP.serve(router, HTTP.Sockets.localhost, 8080)
```

By default all resources are prefixed with `static` in their path. Hence the
use of `/static/**` to serve them from the right route. If resources have the
prefix overridden via the `prefix` keyword then the registered route should be
changed to match this otherwise 404 responses will be returned due to not being
able to find them.

## Experimental web resource bundling

*This feature is subject to change.*

Experimental support for bundling web resources is provided via the `bun`
command-line tool which is provided via the Julia artifacts system and does not
need to be installed manually. The `BundledWebResources.bun` function will
throw an error on that platform currently.

A `watch` function is provided that can register a callback function to be run
each time the `bun build` rebuilds the bundled files. This can be used to
trigger browser reloads or other actions.

## Cookbook

A selection of recipes for common use cases of this package and integrations
with other complementary packages.

### Font Bundling and Loading

Rather than manually downloading and installing fonts to be served by an
application (instead of a CDN) you can use `BundledWebResources` to bundle
fonts into your application and serve them from there.

[Fontsource](https://fontsource.org) is a good source for retrieving fonts from
for this purpose. Locate a font, or several, for example
[Inter](https://fontsource.org/fonts/inter). Navigate to the CDN tab and select
the variants you want to include in your application.

Find the current version of the font (rather than using `latest`, which will
change over time and void the `sha256` hash). This can be found on the `NPM`
link on the font page. Also find the URL to use for the font, which will be
`url()` in the required CSS content. Something like
`https://cdn.jsdelivr.net/fontsource/fonts/inter:vf@latest/latin-wght-normal.woff2`.
Switch the `latest` for the version number. E.g.
`https://cdn.jsdelivr.net/fontsource/fonts/inter:[email protected]/latin-wght-normal.woff2`
so that the `sha256` hash will be stable.

```julia
@register function inter_woff2()
return Resource(
"https://cdn.jsdelivr.net/fontsource/fonts/inter:[email protected]/latin-wght-normal.woff2";
sha256 = "88df0b5a7bc397dbc13a26bb8b3742cc62cd1c9b0dded57da7832416d6f52f42",
)
end
```

Create a `LocalResource` for the fontface CSS file. This will be used to load the
font from the bundled resource.

```julia
function fontfaces_css_content(_dir, _file)
return """
/* inter-latin-wght-normal */
@font-face {
font-family: 'Inter Variable';
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url($(pathof(inter_woff2()))) format('woff2-variations');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
"""
end

@register function fontfaces_css()
return LocalResource(
@__DIR__,
"fontfaces.css",
fontfaces_css_content,
)
end
```

And then reference the `fontfaces.css` file in your HTML. Here using
`HypertextTemplates.jl`:

```julia
@link {rel="stylesheet", href=pathof(fontfaces_css())}
```

Finally, set your font-family to the font name in your CSS.

```css
body {
font-family: "Inter Variable", sans-serif;
}
```

If using Tailwind CSS, for example, you can set the `fontFamily` property in
your `tailwind.config.js` file.

```js
module.exports = {
theme: {
fontFamily: {
sans: ["Inter Variable", "sans-serif"],
},
},
};
```