-
-
Notifications
You must be signed in to change notification settings - Fork 180
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
package/viewer: design viewer API and port svelte's SSR rendering over
- Loading branch information
1 parent
061c0f1
commit baba60b
Showing
19 changed files
with
42,128 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.