| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890 |
- package app
- import (
- "bytes"
- "context"
- "crypto/sha1"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "os"
- "reflect"
- "runtime"
- "sort"
- "strconv"
- "strings"
- "sync"
- "text/template"
- "time"
- "github.com/maxence-charriere/go-app/v9/pkg/errors"
- )
- const (
- defaultThemeColor = "#2d2c2c"
- defaultPreRenderCacheSize = 8000000
- defaultPreRenderCacheTTL = time.Hour * 24
- )
- // Handler is an HTTP handler that serves an HTML page that loads a Go wasm app
- // and its resources.
- type Handler struct {
- // The name of the web application as it is usually displayed to the user.
- Name string
- // The name of the web application displayed to the user when there is not
- // enough space to display Name.
- ShortName string
- // The icon that is used for the PWA, favicon, loading and default not
- // found component.
- Icon Icon
- // A placeholder background color for the application page to display before
- // its stylesheets are loaded.
- //
- // Default: #2d2c2c.
- BackgroundColor string
- // The theme color for the application. This affects how the OS displays the
- // app (e.g., PWA title bar or Android's task switcher).
- //
- // DEFAULT: #2d2c2c.
- ThemeColor string
- // The text displayed while loading a page. Load progress can be inserted by
- // including "{progress}" in the loading label.
- //
- // DEFAULT: "{progress}%".
- LoadingLabel string
- // The page language.
- //
- // DEFAULT: en.
- Lang string
- // The page title.
- Title string
- // The page description.
- Description string
- // The page authors.
- Author string
- // The page keywords.
- Keywords []string
- // The path of the default image that is used by social networks when
- // linking the app.
- Image string
- // The paths or urls of the CSS files to use with the page.
- //
- // eg:
- // app.Handler{
- // Styles: []string{
- // "/web/test.css", // Static resource
- // "https://foo.com/test.css", // External resource
- // },
- // },
- Styles []string
- // The paths or urls of the JavaScript files to use with the page.
- //
- // eg:
- // app.Handler{
- // Scripts: []string{
- // "/web/test.js", // Static resource
- // "https://foo.com/test.js", // External resource
- // },
- // },
- Scripts []string
- // The path of the static resources that the browser is caching in order to
- // provide offline mode.
- //
- // Note that Icon, Styles and Scripts are already cached by default.
- //
- // Paths are relative to the root directory.
- CacheableResources []string
- // Additional headers to be added in head element.
- RawHeaders []string
- // The page HTML element.
- //
- // Default: Html().
- HTML func() HTMLHtml
- // The page body element.
- //
- // Note that the lang attribute is always overridden by the Handler.Lang
- // value.
- //
- // Default: Body().
- Body func() HTMLBody
- // The interval between each app auto-update while running in a web browser.
- // Zero or negative values deactivates the auto-update mechanism.
- //
- // Default is 0.
- AutoUpdateInterval time.Duration
- // The environment variables that are passed to the progressive web app.
- //
- // Reserved keys:
- // - GOAPP_VERSION
- // - GOAPP_GOAPP_STATIC_RESOURCES_URL
- Env Environment
- // The URLs that are launched in the app tab or window.
- //
- // By default, URLs with a different domain are launched in another tab.
- // Specifying internal URLs is to override that behavior. A good use case
- // would be the URL for an OAuth authentication.
- InternalURLs []string
- // The cache that stores pre-rendered pages.
- //
- // Default: A LRU cache that keeps pages up to 24h and have a maximum size
- // of 8MB.
- PreRenderCache PreRenderCache
- // The static resources that are accessible from custom paths. Files that
- // are proxied by default are /robots.txt, /sitemap.xml and /ads.txt.
- ProxyResources []ProxyResource
- // The resource provider that provides static resources. Static resources
- // are always accessed from a path that starts with "/web/".
- //
- // eg:
- // "/web/main.css"
- //
- // Default: LocalDir("")
- Resources ResourceProvider
- // The version number. This is used in order to update the PWA application
- // in the browser. It must be set when deployed on a live system in order to
- // prevent recurring updates.
- //
- // Default: Auto-generated in order to trigger pwa update on a local
- // development system.
- Version string
- // The HTTP header to retrieve the WebAssembly file content length.
- //
- // Content length finding falls back to the Content-Length HTTP header when
- // no content length is found with the defined header.
- WasmContentLengthHeader string
- // The template used to generate app-worker.js. The template follows the
- // text/template package model.
- //
- // By default set to DefaultAppWorkerJS, changing the template have very
- // high chances to mess up go-app usage. Any issue related to a custom app
- // worker template is not supported and will be closed.
- ServiceWorkerTemplate string
- once sync.Once
- etag string
- pwaResources PreRenderCache
- proxyResources map[string]ProxyResource
- }
- func (h *Handler) init() {
- h.initVersion()
- h.initStaticResources()
- h.initImage()
- h.initStyles()
- h.initScripts()
- h.initServiceWorker()
- h.initCacheableResources()
- h.initIcon()
- h.initPWA()
- h.initPageContent()
- h.initPreRenderedResources()
- h.initProxyResources()
- }
- func (h *Handler) initVersion() {
- if h.Version == "" {
- t := time.Now().UTC().String()
- h.Version = fmt.Sprintf(`%x`, sha1.Sum([]byte(t)))
- }
- h.etag = `"` + h.Version + `"`
- }
- func (h *Handler) initStaticResources() {
- if h.Resources == nil {
- h.Resources = LocalDir("")
- }
- }
- func (h *Handler) initImage() {
- if h.Image != "" {
- h.Image = h.resolveStaticPath(h.Image)
- }
- }
- func (h *Handler) initStyles() {
- for i, path := range h.Styles {
- h.Styles[i] = h.resolveStaticPath(path)
- }
- }
- func (h *Handler) initScripts() {
- for i, path := range h.Scripts {
- h.Scripts[i] = h.resolveStaticPath(path)
- }
- }
- func (h *Handler) initServiceWorker() {
- if h.ServiceWorkerTemplate == "" {
- h.ServiceWorkerTemplate = DefaultAppWorkerJS
- }
- }
- func (h *Handler) initCacheableResources() {
- for i, path := range h.CacheableResources {
- h.CacheableResources[i] = h.resolveStaticPath(path)
- }
- }
- func (h *Handler) initIcon() {
- if h.Icon.Default == "" {
- h.Icon.Default = "https://storage.googleapis.com/murlok-github/icon-192.png"
- h.Icon.Large = "https://storage.googleapis.com/murlok-github/icon-512.png"
- }
- if h.Icon.AppleTouch == "" {
- h.Icon.AppleTouch = h.Icon.Default
- }
- h.Icon.Default = h.resolveStaticPath(h.Icon.Default)
- h.Icon.Large = h.resolveStaticPath(h.Icon.Large)
- h.Icon.AppleTouch = h.resolveStaticPath(h.Icon.AppleTouch)
- }
- func (h *Handler) initPWA() {
- if h.Name == "" && h.ShortName == "" && h.Title == "" {
- h.Name = "App PWA"
- }
- if h.ShortName == "" {
- h.ShortName = h.Name
- }
- if h.Name == "" {
- h.Name = h.ShortName
- }
- if h.BackgroundColor == "" {
- h.BackgroundColor = defaultThemeColor
- }
- if h.ThemeColor == "" {
- h.ThemeColor = defaultThemeColor
- }
- if h.Lang == "" {
- h.Lang = "en"
- }
- if h.LoadingLabel == "" {
- h.LoadingLabel = "{progress}%"
- }
- }
- func (h *Handler) initPageContent() {
- if h.HTML == nil {
- h.HTML = Html
- }
- if h.Body == nil {
- h.Body = Body
- }
- }
- func (h *Handler) initPreRenderedResources() {
- h.pwaResources = newPreRenderCache(5)
- ctx := context.TODO()
- h.pwaResources.Set(ctx, PreRenderedItem{
- Path: "/wasm_exec.js",
- ContentType: "application/javascript",
- Body: []byte(wasmExecJS),
- })
- h.pwaResources.Set(ctx, PreRenderedItem{
- Path: "/app.js",
- ContentType: "application/javascript",
- Body: h.makeAppJS(),
- })
- h.pwaResources.Set(ctx, PreRenderedItem{
- Path: "/app-worker.js",
- ContentType: "application/javascript",
- Body: h.makeAppWorkerJS(),
- })
- h.pwaResources.Set(ctx, PreRenderedItem{
- Path: "/manifest.webmanifest",
- ContentType: "application/manifest+json",
- Body: h.makeManifestJSON(),
- })
- h.pwaResources.Set(ctx, PreRenderedItem{
- Path: "/app.css",
- ContentType: "text/css",
- Body: []byte(appCSS),
- })
- if h.PreRenderCache == nil {
- h.PreRenderCache = NewPreRenderLRUCache(
- defaultPreRenderCacheSize,
- defaultPreRenderCacheTTL,
- )
- }
- }
- func (h *Handler) makeAppJS() []byte {
- if h.Env == nil {
- h.Env = make(map[string]string)
- }
- internalURLs, _ := json.Marshal(h.InternalURLs)
- h.Env["GOAPP_INTERNAL_URLS"] = string(internalURLs)
- h.Env["GOAPP_VERSION"] = h.Version
- h.Env["GOAPP_STATIC_RESOURCES_URL"] = h.Resources.Static()
- h.Env["GOAPP_ROOT_PREFIX"] = h.Resources.Package()
- for k, v := range h.Env {
- if err := os.Setenv(k, v); err != nil {
- Log(errors.New("setting app env variable failed").
- Tag("name", k).
- Tag("value", v).
- Wrap(err))
- }
- }
- var b bytes.Buffer
- if err := template.
- Must(template.New("app.js").Parse(appJS)).
- Execute(&b, struct {
- Env string
- LoadingLabel string
- Wasm string
- WasmContentLengthHeader string
- WorkerJS string
- AutoUpdateInterval int64
- }{
- Env: jsonString(h.Env),
- LoadingLabel: h.LoadingLabel,
- Wasm: h.Resources.AppWASM(),
- WasmContentLengthHeader: h.WasmContentLengthHeader,
- WorkerJS: h.resolvePackagePath("/app-worker.js"),
- AutoUpdateInterval: h.AutoUpdateInterval.Milliseconds(),
- }); err != nil {
- panic(errors.New("initializing app.js failed").Wrap(err))
- }
- return b.Bytes()
- }
- func (h *Handler) makeAppWorkerJS() []byte {
- resources := make(map[string]struct{})
- setResources := func(res ...string) {
- for _, r := range res {
- if r == "" {
- continue
- }
- resources[r] = struct{}{}
- }
- }
- setResources(
- h.resolvePackagePath("/app.css"),
- h.resolvePackagePath("/app.js"),
- h.resolvePackagePath("/manifest.webmanifest"),
- h.resolvePackagePath("/wasm_exec.js"),
- h.resolvePackagePath("/"),
- h.Resources.AppWASM(),
- )
- setResources(h.Icon.Default, h.Icon.Large, h.Icon.AppleTouch)
- setResources(h.Styles...)
- setResources(h.Scripts...)
- setResources(h.CacheableResources...)
- resourcesTocache := make([]string, 0, len(resources))
- for k := range resources {
- resourcesTocache = append(resourcesTocache, k)
- }
- sort.Slice(resourcesTocache, func(a, b int) bool {
- return strings.Compare(resourcesTocache[a], resourcesTocache[b]) < 0
- })
- var b bytes.Buffer
- if err := template.
- Must(template.New("app-worker.js").Parse(h.ServiceWorkerTemplate)).
- Execute(&b, struct {
- Version string
- ResourcesToCache string
- }{
- Version: h.Version,
- ResourcesToCache: jsonString(resourcesTocache),
- }); err != nil {
- panic(errors.New("initializing app-worker.js failed").Wrap(err))
- }
- return b.Bytes()
- }
- func (h *Handler) makeManifestJSON() []byte {
- normalize := func(s string) string {
- if !strings.HasPrefix(s, "/") {
- s = "/" + s
- }
- if !strings.HasSuffix(s, "/") {
- s += "/"
- }
- return s
- }
- var b bytes.Buffer
- if err := template.
- Must(template.New("manifest.webmanifest").Parse(manifestJSON)).
- Execute(&b, struct {
- ShortName string
- Name string
- Description string
- DefaultIcon string
- LargeIcon string
- BackgroundColor string
- ThemeColor string
- Scope string
- StartURL string
- }{
- ShortName: h.ShortName,
- Name: h.Name,
- Description: h.Description,
- DefaultIcon: h.Icon.Default,
- LargeIcon: h.Icon.Large,
- BackgroundColor: h.BackgroundColor,
- ThemeColor: h.ThemeColor,
- Scope: normalize(h.Resources.Package()),
- StartURL: normalize(h.Resources.Package()),
- }); err != nil {
- panic(errors.New("initializing manifest.webmanifest failed").Wrap(err))
- }
- return b.Bytes()
- }
- func (h *Handler) initProxyResources() {
- resources := make(map[string]ProxyResource)
- for _, r := range h.ProxyResources {
- switch r.Path {
- case "/wasm_exec.js",
- "/goapp.js",
- "/app.js",
- "/app-worker.js",
- "/manifest.json",
- "/manifest.webmanifest",
- "/app.css",
- "/app.wasm",
- "/goapp.wasm",
- "/":
- continue
- default:
- if strings.HasPrefix(r.Path, "/") && strings.HasPrefix(r.ResourcePath, "/web/") {
- resources[r.Path] = r
- }
- }
- }
- if _, ok := resources["/robots.txt"]; !ok {
- resources["/robots.txt"] = ProxyResource{
- Path: "/robots.txt",
- ResourcePath: "/web/robots.txt",
- }
- }
- if _, ok := resources["/sitemap.xml"]; !ok {
- resources["/sitemap.xml"] = ProxyResource{
- Path: "/sitemap.xml",
- ResourcePath: "/web/sitemap.xml",
- }
- }
- if _, ok := resources["/ads.txt"]; !ok {
- resources["/ads.txt"] = ProxyResource{
- Path: "/ads.txt",
- ResourcePath: "/web/ads.txt",
- }
- }
- h.proxyResources = resources
- }
- func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- h.once.Do(h.init)
- w.Header().Set("Cache-Control", "no-cache")
- w.Header().Set("ETag", h.etag)
- etag := r.Header.Get("If-None-Match")
- if etag == h.etag {
- w.WriteHeader(http.StatusNotModified)
- return
- }
- path := r.URL.Path
- fileHandler, isServingStaticResources := h.Resources.(http.Handler)
- if isServingStaticResources && strings.HasPrefix(path, "/web/") {
- fileHandler.ServeHTTP(w, r)
- return
- }
- switch path {
- case "/goapp.js":
- path = "/app.js"
- case "/manifest.json":
- path = "/manifest.webmanifest"
- case "/app.wasm", "/goapp.wasm":
- if isServingStaticResources {
- r2 := *r
- r2.URL.Path = h.Resources.AppWASM()
- fileHandler.ServeHTTP(w, &r2)
- return
- }
- w.WriteHeader(http.StatusNotFound)
- return
- }
- if res, ok := h.pwaResources.Get(r.Context(), path); ok {
- h.servePreRenderedItem(w, res)
- return
- }
- if res, ok := h.PreRenderCache.Get(r.Context(), path); ok {
- h.servePreRenderedItem(w, res)
- return
- }
- if proxyResource, ok := h.proxyResources[path]; ok {
- h.serveProxyResource(proxyResource, w, r)
- return
- }
- h.servePage(w, r)
- }
- func (h *Handler) servePreRenderedItem(w http.ResponseWriter, r PreRenderedItem) {
- w.Header().Set("Content-Length", strconv.Itoa(r.Size()))
- w.Header().Set("Content-Type", r.ContentType)
- if r.ContentEncoding != "" {
- w.Header().Set("Content-Encoding", r.ContentEncoding)
- }
- w.WriteHeader(http.StatusOK)
- w.Write(r.Body)
- }
- func (h *Handler) serveProxyResource(resource ProxyResource, w http.ResponseWriter, r *http.Request) {
- var u string
- if _, ok := h.Resources.(http.Handler); ok {
- var protocol string
- if r.TLS != nil {
- protocol = "https://"
- } else {
- protocol = "http://"
- }
- u = protocol + r.Host + resource.ResourcePath
- } else {
- u = h.Resources.Static() + resource.ResourcePath
- }
- res, err := http.Get(u)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- Log(errors.New("getting proxy static resource failed").
- Tag("url", u).
- Tag("proxy-path", resource.Path).
- Tag("static-resource-path", resource.ResourcePath).
- Wrap(err),
- )
- return
- }
- defer res.Body.Close()
- if res.StatusCode != http.StatusOK {
- w.WriteHeader(http.StatusNotFound)
- return
- }
- body, err := io.ReadAll(res.Body)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- Log(errors.New("reading proxy static resource failed").
- Tag("url", u).
- Tag("proxy-path", resource.Path).
- Tag("static-resource-path", resource.ResourcePath).
- Wrap(err),
- )
- return
- }
- item := PreRenderedItem{
- Path: resource.Path,
- ContentType: res.Header.Get("Content-Type"),
- ContentEncoding: res.Header.Get("Content-Encoding"),
- Body: body,
- }
- h.PreRenderCache.Set(r.Context(), item)
- h.servePreRenderedItem(w, item)
- }
- func (h *Handler) servePage(w http.ResponseWriter, r *http.Request) {
- content, ok := routes.createComponent(r.URL.Path)
- if !ok {
- http.NotFound(w, r)
- return
- }
- url := *r.URL
- url.Host = r.Host
- url.Scheme = "http"
- var page requestPage
- page.SetTitle(h.Title)
- page.SetLang(h.Lang)
- page.SetDescription(h.Description)
- page.SetAuthor(h.Author)
- page.SetKeywords(h.Keywords...)
- page.SetLoadingLabel(strings.ReplaceAll(h.LoadingLabel, "{progress}", "0"))
- page.SetImage(h.Image)
- page.url = &url
- disp := engine{
- Page: &page,
- IsServerSide: true,
- StaticResourceResolver: h.resolveStaticPath,
- ActionHandlers: actionHandlers,
- }
- body := h.Body().privateBody(
- Div().Body(
- Aside().
- ID("app-wasm-loader").
- Class("goapp-app-info").
- Body(
- Img().
- ID("app-wasm-loader-icon").
- Class("goapp-logo goapp-spin").
- Src(h.Icon.Default),
- P().
- ID("app-wasm-loader-label").
- Class("goapp-label").
- Text(page.loadingLabel),
- ),
- Div().ID("app-pre-render").Body(content),
- ),
- )
- if err := mount(&disp, body); err != nil {
- panic(errors.New("mounting pre-rendering container failed").
- Tag("server-side", disp.isServerSide()).
- Tag("body-type", reflect.TypeOf(disp.Body)).
- Wrap(err))
- }
- disp.Body = body
- disp.init()
- defer disp.Close()
- disp.PreRender()
- for len(disp.dispatches) != 0 {
- disp.Consume()
- disp.Wait()
- }
- var b bytes.Buffer
- b.WriteString("<!DOCTYPE html>\n")
- PrintHTML(&b, h.HTML().
- Lang(page.Lang()).
- privateBody(
- Head().Body(
- Meta().Charset("UTF-8"),
- Meta().
- Name("author").
- Content(page.Author()),
- Meta().
- Name("description").
- Content(page.Description()),
- Meta().
- Name("keywords").
- Content(page.Keywords()),
- Meta().
- Name("theme-color").
- Content(h.ThemeColor),
- Meta().
- Name("viewport").
- Content("width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover"),
- Meta().
- Property("og:url").
- Content(page.URL().String()),
- Meta().
- Property("og:title").
- Content(page.Title()),
- Meta().
- Property("og:description").
- Content(page.Description()),
- Meta().
- Property("og:type").
- Content("website"),
- Meta().
- Property("og:image").
- Content(page.Image()),
- Title().Text(page.Title()),
- Link().
- Rel("icon").
- Href(h.Icon.Default),
- If(h.Icon.Mask != "",
- Link().
- Rel("mask-icon").
- Attr("color", h.Icon.MaskColor).
- Href(h.Icon.Mask),
- ),
- Link().
- Rel("apple-touch-icon").
- Href(h.Icon.AppleTouch),
- Link().
- Rel("manifest").
- Href(h.resolvePackagePath("/manifest.webmanifest")),
- Link().
- Type("text/css").
- Rel("stylesheet").
- Href(h.resolvePackagePath("/app.css")),
- Script().
- Defer(true).
- Src(h.resolvePackagePath("/wasm_exec.js")),
- Script().
- Defer(true).
- Src(h.resolvePackagePath("/app.js")),
- Range(h.Styles).Slice(func(i int) UI {
- return Link().
- Type("text/css").
- Rel("stylesheet").
- Href(h.Styles[i])
- }),
- Range(h.Scripts).Slice(func(i int) UI {
- return Script().
- Defer(true).
- Src(h.Scripts[i])
- }),
- Range(h.RawHeaders).Slice(func(i int) UI {
- return Raw(h.RawHeaders[i])
- }),
- ),
- body,
- ))
- item := PreRenderedItem{
- Path: page.URL().Path,
- Body: b.Bytes(),
- ContentType: "text/html",
- }
- h.PreRenderCache.Set(r.Context(), item)
- h.servePreRenderedItem(w, item)
- }
- func (h *Handler) resolvePackagePath(path string) string {
- var b strings.Builder
- b.WriteByte('/')
- appResources := strings.Trim(h.Resources.Package(), "/")
- b.WriteString(appResources)
- path = strings.Trim(path, "/")
- if b.Len() != 1 && path != "" {
- b.WriteByte('/')
- }
- b.WriteString(path)
- return b.String()
- }
- func (h *Handler) resolveStaticPath(path string) string {
- if isRemoteLocation(path) || !isStaticResourcePath(path) {
- return path
- }
- var b strings.Builder
- staticResources := strings.TrimSuffix(h.Resources.Static(), "/")
- b.WriteString(staticResources)
- path = strings.Trim(path, "/")
- b.WriteByte('/')
- b.WriteString(path)
- return b.String()
- }
- // Icon describes a square image that is used in various places such as
- // application icon, favicon or loading icon.
- type Icon struct {
- // The path or url to a square image/png file. It must have a side of 192px.
- //
- // Path is relative to the root directory.
- Default string
- // The path or url to larger square image/png file. It must have a side of
- // 512px.
- //
- // Path is relative to the root directory.
- Large string
- // The path or url to a square image/png file that is used for IOS/IPadOS
- // home screen icon. It must have a side of 192px.
- //
- // Path is relative to the root directory.
- //
- // DEFAULT: Icon.Default
- AppleTouch string
- Mask string
- MaskColor string
- }
- // Environment describes the environment variables to pass to the progressive
- // web app.
- type Environment map[string]string
- func normalizeFilePath(path string) string {
- if runtime.GOOS == "windows" {
- return strings.ReplaceAll(path, "/", `\`)
- }
- return path
- }
- func isRemoteLocation(path string) bool {
- return strings.HasPrefix(path, "https://") ||
- strings.HasPrefix(path, "http://")
- }
- func isStaticResourcePath(path string) bool {
- return strings.HasPrefix(path, "/web/") ||
- strings.HasPrefix(path, "web/")
- }
- type httpResource struct {
- Path string
- ContentType string
- Body []byte
- ExpireAt time.Time
- }
- func (r httpResource) Len() int {
- return len(r.Body)
- }
- func (r httpResource) IsExpired() bool {
- return r.ExpireAt != time.Time{} && r.ExpireAt.Before(time.Now())
- }
|