Skip to content

Commit

Permalink
package/viewer: design viewer API and port svelte's SSR rendering over
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewmueller committed Feb 6, 2023
1 parent 061c0f1 commit baba60b
Show file tree
Hide file tree
Showing 19 changed files with 42,128 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/keegancsmith/rpc v1.3.0
github.com/lithammer/dedent v1.1.0
github.com/livebud/bud-test-plugin v0.0.9
github.com/livebud/js v0.0.0-20221112072017-b9e63b92aad5
github.com/livebud/transpiler v0.0.1
github.com/matthewmueller/diff v0.0.0-20220104030700-cb2fe910d90c
github.com/matthewmueller/gotext v0.0.0-20210424201144-265ed61725ac
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ github.com/livebud/bud-test-nested-plugin v0.0.5 h1:MpPp20Gng0F+Kvl+L9kttu6nsJIY
github.com/livebud/bud-test-nested-plugin v0.0.5/go.mod h1:M3QujkGG4ggZ6h75t5zF8MEJFrLTwa2USeIYHQdO2YQ=
github.com/livebud/bud-test-plugin v0.0.9 h1:JmS4aj+NV52RUroteLs+ld6rcbkBwio7p9qPNutTsqM=
github.com/livebud/bud-test-plugin v0.0.9/go.mod h1:GTxMZ8W4BIyGIOgAA4hvPHMDDTkaZtfcuhnOcSu3y8M=
github.com/livebud/js v0.0.0-20221112072017-b9e63b92aad5 h1:7fYJMOnT4WrnjRhJ8B6HiJBGcDVFd9jam0205gn9y1k=
github.com/livebud/js v0.0.0-20221112072017-b9e63b92aad5/go.mod h1:TDkks+mVAlB96mxcbIAftAxpGteCxoBYGVF1JThjcLk=
github.com/livebud/transpiler v0.0.1 h1:raG4W8qMq534VYrWv5sFQieQCa4McrJhQOy0YXk63kk=
github.com/livebud/transpiler v0.0.1/go.mod h1:vQYMN//Y2cnM55tw0lOmLGbEETugP7alxTyhQHzNdTI=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
Expand Down
94 changes: 94 additions & 0 deletions internal/es/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package es

import (
"fmt"
"io/fs"
"strings"

esbuild "github.com/evanw/esbuild/pkg/api"
)

// TODO: replace with *gomod.Module
func New(absDir string) *Builder {
return &Builder{
dir: absDir,
base: esbuild.BuildOptions{
AbsWorkingDir: absDir,
Outdir: "./",
Format: esbuild.FormatIIFE,
Platform: esbuild.PlatformBrowser,
GlobalName: "bud",
Bundle: true,
Plugins: []esbuild.Plugin{
httpPlugin(absDir),
esmPlugin(absDir),
},
},
}
}

type Builder struct {
dir string
base esbuild.BuildOptions
}

func (b *Builder) Directory() string {
return b.dir
}

type Entrypoint = esbuild.EntryPoint
type Plugin = esbuild.Plugin

type Build struct {
Entrypoint string
Plugins []Plugin
Minify bool
}

func (b *Builder) Build(build *Build) ([]byte, error) {
input := esbuild.BuildOptions{
EntryPointsAdvanced: []esbuild.EntryPoint{
{
InputPath: build.Entrypoint,
OutputPath: build.Entrypoint,
},
},
AbsWorkingDir: b.base.AbsWorkingDir,
Outdir: b.base.Outdir,
Format: b.base.Format,
Platform: b.base.Platform,
GlobalName: b.base.GlobalName,
Bundle: b.base.Bundle,
Metafile: b.base.Metafile,
Plugins: append(build.Plugins, b.base.Plugins...),
}
if build.Minify {
input.MinifyWhitespace = true
input.MinifyIdentifiers = true
input.MinifySyntax = true
}
result := esbuild.Build(input)
if len(result.Errors) > 0 {
msgs := esbuild.FormatMessages(result.Errors, esbuild.FormatMessagesOptions{
Color: true,
Kind: esbuild.ErrorMessage,
})
return nil, fmt.Errorf(strings.Join(msgs, "\n"))
}
// Expect exactly 1 output file
if len(result.OutputFiles) != 1 {
return nil, fmt.Errorf("expected exactly 1 output file but got %d", len(result.OutputFiles))
}
ssrCode := result.OutputFiles[0].Contents
return ssrCode, nil
}

type Bundle struct {
Entrypoints []string
Plugins []Plugin
Minify bool
}

func (b *Builder) Bundle(out fs.FS, bundle *Bundle) error {
return nil
}
66 changes: 66 additions & 0 deletions internal/es/plugins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package es

import (
"io"
"net/http"

esbuild "github.com/evanw/esbuild/pkg/api"
)

func httpPlugin(absDir string) esbuild.Plugin {
return esbuild.Plugin{
Name: "bud-http",
Setup: func(epb esbuild.PluginBuild) {
epb.OnResolve(esbuild.OnResolveOptions{Filter: `^http[s]?://`}, func(args esbuild.OnResolveArgs) (result esbuild.OnResolveResult, err error) {
result.Namespace = "http"
result.Path = args.Path
return result, nil
})
epb.OnLoad(esbuild.OnLoadOptions{Filter: `.*`, Namespace: `http`}, func(args esbuild.OnLoadArgs) (result esbuild.OnLoadResult, err error) {
res, err := http.Get(args.Path)
if err != nil {
return result, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return result, err
}
contents := string(body)
result.ResolveDir = absDir
result.Contents = &contents
result.Loader = esbuild.LoaderJS
return result, nil
})
},
}
}

func esmPlugin(absDir string) esbuild.Plugin {
return esbuild.Plugin{
Name: "bud-esm",
Setup: func(epb esbuild.PluginBuild) {
epb.OnResolve(esbuild.OnResolveOptions{Filter: `^[a-z0-9@]`}, func(args esbuild.OnResolveArgs) (result esbuild.OnResolveResult, err error) {
result.Namespace = "esm"
result.Path = args.Path
return result, nil
})
epb.OnLoad(esbuild.OnLoadOptions{Filter: `.*`, Namespace: `esm`}, func(args esbuild.OnLoadArgs) (result esbuild.OnLoadResult, err error) {
res, err := http.Get("https://esm.sh/" + args.Path)
if err != nil {
return result, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return result, err
}
contents := string(body)
result.ResolveDir = absDir
result.Contents = &contents
result.Loader = esbuild.LoaderJS
return result, nil
})
},
}
}
100 changes: 100 additions & 0 deletions package/viewer/find.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package viewer

import (
"io/fs"
"path"
"path/filepath"
)

type Pages = map[Key]*Page

// Find pages
func Find(fsys fs.FS) (Pages, error) {
pages := make(Pages)
inherited := &inherited{
Layout: make(map[ext]*View),
Frames: make(map[ext][]*View),
Error: make(map[ext]*View),
}
if err := find(fsys, pages, inherited, "."); err != nil {
return nil, err
}
return pages, nil
}

type ext = string

type inherited struct {
Layout map[ext]*View
Frames map[ext][]*View
Error map[ext]*View
}

func find(fsys fs.FS, pages Pages, inherited *inherited, dir string) error {
des, err := fs.ReadDir(fsys, dir)
if err != nil {
return err
}

// First pass: look for layouts, frames and errors
for _, de := range des {
if de.IsDir() {
continue
}
ext := filepath.Ext(de.Name())
extless := de.Name()[:len(de.Name())-len(ext)]
switch extless {
case "layout":
inherited.Layout[ext] = &View{
Path: path.Join(dir, de.Name()),
Key: path.Join(dir, extless),
}
case "frame":
inherited.Frames[ext] = append(inherited.Frames[ext], &View{
Path: path.Join(dir, de.Name()),
Key: path.Join(dir, extless),
})
case "error":
inherited.Error[ext] = &View{
Path: path.Join(dir, de.Name()),
Key: path.Join(dir, extless),
}
}
}

// Second pass: go through pages
for _, de := range des {
if de.IsDir() {
continue
}
ext := filepath.Ext(de.Name())
extless := de.Name()[:len(de.Name())-len(ext)]
switch extless {
case "layout", "frame", "error":
continue
default:
key := path.Join(dir, extless)
pages[key] = &Page{
View: &View{
Path: path.Join(dir, de.Name()),
Key: key,
},
Layout: inherited.Layout[ext],
Frames: inherited.Frames[ext],
Error: inherited.Error[ext],
}
}
}

// Third pass: go through directories
for _, de := range des {
if !de.IsDir() {
continue
}
if err := find(fsys, pages, inherited, de.Name()); err != nil {
return err
}
}

return nil
}
57 changes: 57 additions & 0 deletions package/viewer/find_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package viewer_test

import (
"fmt"
"testing"
"testing/fstest"

"github.com/livebud/bud/internal/is"
"github.com/livebud/bud/package/viewer"
)

func TestIndex(t *testing.T) {
is := is.New(t)
fsys := fstest.MapFS{
"index.gohtml": &fstest.MapFile{Data: []byte("Hello {{ .Planet }}!")},
}
// Find the pages
pages, err := viewer.Find(fsys)
is.NoErr(err)
is.Equal(len(pages), 1)
is.True(pages["index"] != nil)
is.Equal(pages["index"].Path, "index.gohtml")
is.Equal(len(pages["index"].Frames), 0)
is.Equal(pages["index"].Layout, nil)
is.Equal(pages["index"].Error, nil)
}

func TestNested(t *testing.T) {
is := is.New(t)
fsys := fstest.MapFS{
"layout.svelte": &fstest.MapFile{Data: []byte(`<slot />`)},
"frame.svelte": &fstest.MapFile{Data: []byte(`<slot />`)},
"posts/frame.svelte": &fstest.MapFile{Data: []byte(`<slot />`)},
"posts/index.svelte": &fstest.MapFile{Data: []byte(`<h1>Hello {planet}!</h1>`)},
}
// Find the pages
pages, err := viewer.Find(fsys)
is.NoErr(err)
is.Equal(len(pages), 1)
fmt.Println(pages)
is.True(pages["posts/index"] != nil)
is.Equal(pages["posts/index"].Path, "posts/index.svelte")

// Frames
is.Equal(len(pages["posts/index"].Frames), 2)
is.Equal(pages["posts/index"].Frames[0].Key, "frame")
is.Equal(pages["posts/index"].Frames[0].Path, "frame.svelte")
is.Equal(pages["posts/index"].Frames[1].Key, "posts/frame")
is.Equal(pages["posts/index"].Frames[1].Path, "posts/frame.svelte")

is.Equal(pages["posts/index"].Error, nil)

// Layout
is.True(pages["posts/index"].Layout != nil)
is.Equal(pages["posts/index"].Layout.Key, "layout")
is.Equal(pages["posts/index"].Layout.Path, "layout.svelte")
}
60 changes: 60 additions & 0 deletions package/viewer/gohtml/gohtml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package gohtml

import (
"bytes"
"context"
"fmt"
"html/template"
"io/fs"

"github.com/livebud/bud/package/viewer"
"github.com/livebud/bud/runtime/transpiler"
)

func New(fsys fs.FS, transpiler transpiler.Interface, pages viewer.Pages) *Viewer {
return &Viewer{fsys, pages, transpiler}
}

type Viewer struct {
fsys fs.FS
pages viewer.Pages
transpiler transpiler.Interface
}

var _ viewer.Viewer = (*Viewer)(nil)

func (v *Viewer) Register(router viewer.Router) {
fmt.Println("register called")
}

func (v *Viewer) Render(ctx context.Context, key string, props viewer.Props) ([]byte, error) {
page, ok := v.pages[key]
if !ok {
return nil, fmt.Errorf("gohtml: %q. %w", key, viewer.ErrPageNotFound)
}
entryCode, err := fs.ReadFile(v.fsys, page.Path)
if err != nil {
return nil, fmt.Errorf("gohtml: error reading %q. %w", page.Path, err)
}
entryCode, err = v.transpiler.Transpile(page.Path, ".gohtml", entryCode)
if err != nil {
return nil, fmt.Errorf("gohtml: error transpiling %q. %w", page.Path, err)
}
entryTemplate, err := template.New(page.Path).Parse(string(entryCode))
if err != nil {
return nil, fmt.Errorf("gohtml: error parsing %q. %w", page.Path, err)
}
entryHTML := new(bytes.Buffer)
if err := entryTemplate.Execute(entryHTML, props[page.Key]); err != nil {
return nil, fmt.Errorf("gohtml: error executing %q. %w", page.Path, err)
}
return entryHTML.Bytes(), nil
}

func (v *Viewer) RenderError(ctx context.Context, key string, err error, props viewer.Props) []byte {
return []byte("RenderError not implemented")
}

func (v *Viewer) Bundle(ctx context.Context, out viewer.FS) error {
return nil
}
Loading

0 comments on commit baba60b

Please sign in to comment.