Documentation
¶
Overview ¶
Package page describes which islands a page mounts and where, as a Spec the server writes into the page for the browser to read (tsappkit's island page mounts each island from a registry by name).
A layout template places the slots (elements with data-slot); the Spec says which island goes in each, how it's presented, and its config. The partial templates/page/Islands.html writes it as <script type="application/json" id="page-spec">.
An app that needs more in its spec (the things a page starts with, say) embeds Spec in its own type and writes it with ScriptJSON:
type Spec struct {
page.Spec
Things []Thing `json:"things"`
}
func (s Spec) JSON() template.JS {
c := s
c.Spec = s.Spec.Normalized()
return page.ScriptJSON(c)
}
encoding/json flattens the embedded Spec, so the browser sees one object with layout, islands and things.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CheckIslands ¶
CheckIslands reports every island the specs name that isn't in known, the names the browser's registry can mount. The page itself only finds out at runtime, with a console warning on whichever page names the missing island, so this is for a test that walks an app's specs:
known := append(assets.Names(), "toolbar") // lazy entries, plus those bundled with the entry
if err := page.CheckIslands(known, homeSpec, gameSpec); err != nil {
t.Fatal(err)
}
It returns nil when every name is known, and otherwise one error listing each unknown name once, sorted, with the layouts that use it. It returns an error rather than taking a *testing.T so the page package doesn't import testing, which would add the test flags to every binary that imports page.
func ScriptJSON ¶
ScriptJSON is v as JSON that's safe inside a <script> element: every '<', '>' and '&' is escaped, so no string in it can close the script or open a comment. A value that can't be encoded is written as null.
Types ¶
type Assets ¶
type Assets struct {
// Entries are the page scripts (esbuild's entry points), by name.
Entries map[string]Chunk `json:"entries"`
// Islands are the lazy islands' chunks, by island name.
Islands map[string]Chunk `json:"islands"`
}
Assets says which built files a page and its islands need, so a page can write <link rel="modulepreload"> for what it loads at once (see For and the IslandPreloads partial in templates/page/Islands.html). Each lazy island (tsappkit's lazy) is its own chunk, and code an island shares with the page's entry script goes into chunks the entry imports. Without preload links the browser only finds those chunks, and then their imports, after the entry has arrived and been parsed.
The JSON form is the shape any bundler's build can write:
{"entries": {"main": {"file": "/static/main.js", "imports": ["/static/chunks/chunk-Z.js"]}},
"islands": {"hero": {"file": "/static/chunks/hero-X.js", "imports": ["/static/chunks/chunk-Y.js"]}}}
LoadAssets reads that; LoadEsbuildMetafile builds it from esbuild's metafile.
func LoadAssets ¶
LoadAssets reads Assets in its JSON form from a file.
func LoadEsbuildMetafile ¶
func LoadEsbuildMetafile(file string, opts EsbuildOptions) (*Assets, error)
LoadEsbuildMetafile builds Assets from the metafile of an esbuild build run with --splitting --format=esm --metafile. An output with a source file that some other output reaches through a dynamic import() is an island chunk; any other output with a source file is an entry. Both are named by opts.Name, and their Imports are the chunks they reach through static imports. Two islands, or two entries, with the same name are an error, since the page couldn't tell which one it means.
func (*Assets) For ¶
For lists the chunks to preload on a page whose script is the entry named entry and whose islands are spec's: the entry's imports first, then each island that mounts at once (Load "" or "eager") followed by its imports, in spec order and without repeats. The entry file itself is left out, since the page's <script> loads it. Islands that wait (idle, visible, media) are left out too, since preloading them would download what the page means to put off.
An entry or island Assets doesn't know adds nothing (an island bundled with the entry, say), and a nil Assets gives nothing, so a page can call it whether or not the app loaded a manifest.
type Chunk ¶
Chunk is one built script and what it needs. File is its URL; Imports are the URLs of every chunk it statically imports, directly or through another chunk, in a stable order. They never include a chunk reached only through a dynamic import(), which is another island and loads when that island does.
type EsbuildOptions ¶
type EsbuildOptions struct {
// OutDir is the output directory as the metafile writes it (the --outdir
// relative to where esbuild ran, "dist" say). It's cut from the front of
// each output path before URLPrefix is added.
OutDir string
// URLPrefix is where the page serves OutDir, such as "/static/".
URLPrefix string
// Name gives the entry or island name for an output's source file (its
// path as the metafile has it, "web/islands/hero.ts"). The default is the
// file's base name without its extension ("hero", or "main" for
// web/main.ts), so a registry entry `hero: lazy(() => import("./islands/hero"))`
// needs no setting. Returning "" leaves that output out.
Name func(source string) string
}
EsbuildOptions says how LoadEsbuildMetafile turns the metafile's paths into entry and island names and URLs.
type Island ¶
type Island struct {
Name string `json:"name"`
Slot string `json:"slot"`
Presentation string `json:"presentation,omitempty"`
Config map[string]any `json:"config"`
// Load says when the browser mounts the island: "eager" (the default,
// also written as ""), "idle" once the page has settled, "visible" when
// its slot scrolls into view, or "media:<query>" while the media query
// matches. Each mounts once. tsappkit's IslandPage (0.6.0 on) waits for
// it; a deferred island mustn't be something another island or the page
// needs at startup, since nothing waits for it.
Load string `json:"load,omitempty"`
}
Island is one island on the page. Name picks the factory from the browser's registry; Slot is the data-slot of the element it mounts in. Presentation says how the layout shows it (an app's own words: "page", "panel", "strip"), for islands that draw differently in each. Config is handed to the factory as is, so it must be JSON.
type Spec ¶
type Spec struct {
// Layout names the arrangement of slots, for code that keeps state per
// layout. The template that draws it is chosen by the page, not by this.
Layout string `json:"layout"`
Islands []Island `json:"islands"`
}
Spec is a page's layout and the islands it mounts, in mount order.
func (Spec) JSON ¶
JSON is the spec for a <script type="application/json"> element, written through ScriptJSON after Normalized.
func (Spec) Normalized ¶
Normalized is a copy of the spec with a nil island list as empty and each nil Config as an empty map, so the browser always gets a list and objects rather than null. The spec itself is left alone.
func (Spec) Validate ¶
Validate reports a spec the browser couldn't mount as meant: no layout, an island without a name or with a slot name that isn't lowercase letters, digits and dashes, a load that isn't one of Island.Load's forms, or two islands in one slot. An app that extends the spec calls it from its own Validate and adds its own checks.