Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions go/display/http/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build !js

// SPDX-Licence-Identifier: EUPL-1.2

// Package httpdisplay serves server-rendered TypeScript output and static
// assets over HTTP.
package httpdisplay
93 changes: 93 additions & 0 deletions go/display/http/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//go:build !js

// SPDX-Licence-Identifier: EUPL-1.2

package httpdisplay

import (
nethttp "net/http"

core "dappco.re/go"
tsengine "dappco.re/go/html/engine/ts"
)

// Option configures Handler.
type Option func(*handlerOptions)

type handlerOptions struct {
entry string
}

// WithEntry selects the TypeScript or JavaScript SSR entry module.
func WithEntry(entry string) Option {
return func(options *handlerOptions) {
options.entry = entry
}
}

// Handler returns an HTTP handler that serves files found under assetsDir and
// server-renders every other request through engine.
//
// Static files retain their standard content types and caching semantics.
// Rendered responses receive text/html; charset=utf-8.
func Handler(engine tsengine.Renderer, assetsDir string, opts ...Option) nethttp.Handler {
options := handlerOptions{}
for _, option := range opts {
if option != nil {
option(&options)
}
}

var assets nethttp.Handler
if assetsDir != "" {
assets = nethttp.FileServer(nethttp.Dir(assetsDir))
}

return nethttp.HandlerFunc(func(writer nethttp.ResponseWriter, request *nethttp.Request) {
if assets != nil && isAssetRequest(assetsDir, request) {
assets.ServeHTTP(writer, request)
return
}
if engine == nil {
nethttp.Error(writer, "render engine unavailable", nethttp.StatusInternalServerError)
return
}
if core.Trim(options.entry) == "" {
nethttp.Error(writer, "render entry unavailable", nethttp.StatusInternalServerError)
return
}

input := map[string]any{
"method": request.Method,
"url": request.URL.String(),
"path": request.URL.Path,
"query": request.URL.Query(),
"headers": request.Header.Clone(),
"host": request.Host,
}
output, err := engine.Render(request.Context(), options.entry, input)
if err != nil {
nethttp.Error(writer, "render failed", nethttp.StatusInternalServerError)
return
}

writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(nethttp.StatusOK)
_, _ = writer.Write(output)
})
}

func isAssetRequest(assetsDir string, request *nethttp.Request) bool {
if request.Method != nethttp.MethodGet && request.Method != nethttp.MethodHead {
return false
}

file, err := nethttp.Dir(assetsDir).Open(request.URL.Path)
if err != nil {
return false
}
defer file.Close()

info, err := file.Stat()
return err == nil && !info.IsDir()
}
13 changes: 13 additions & 0 deletions go/display/http/http_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build !js

// SPDX-Licence-Identifier: EUPL-1.2

package httpdisplay

func ExampleHandler() {
_ = Handler
}

func ExampleWithEntry() {
_ = WithEntry("server.ts")
}
112 changes: 112 additions & 0 deletions go/display/http/http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//go:build !js

// SPDX-Licence-Identifier: EUPL-1.2

package httpdisplay

import (
nethttp "net/http"
"net/http/httptest"
"testing"

core "dappco.re/go"
)

type renderStub struct {
output []byte
err error
calls int
entry string
input any
}

func (s *renderStub) Render(_ core.Context, entry string, input any) ([]byte, error) {
s.calls++
s.entry = entry
s.input = input
return s.output, s.err
}

func TestHttp_Handler_Good(t *testing.T) {
renderer := &renderStub{output: []byte("<main>rendered</main>")}
handler := Handler(renderer, "", WithEntry("server.ts"))
recorder := httptest.NewRecorder()
request := httptest.NewRequest(nethttp.MethodGet, "http://example.test/dashboard?tab=one", nil)

handler.ServeHTTP(recorder, request)

core.AssertEqual(t, nethttp.StatusOK, recorder.Code)
core.AssertEqual(t, "<main>rendered</main>", recorder.Body.String())
core.AssertEqual(t, "text/html; charset=utf-8", recorder.Header().Get("Content-Type"))
core.AssertEqual(t, 1, renderer.calls)
requestInput := renderer.input.(map[string]any)
core.AssertEqual(t, "/dashboard", requestInput["path"])
}

func TestHttp_Handler_Bad(t *testing.T) {
renderer := &renderStub{err: core.E("test.render", "failed", nil)}
handler := Handler(renderer, "", WithEntry("broken.ts"))
recorder := httptest.NewRecorder()
request := httptest.NewRequest(nethttp.MethodGet, "http://example.test/", nil)

handler.ServeHTTP(recorder, request)

core.AssertEqual(t, nethttp.StatusInternalServerError, recorder.Code)
core.AssertContains(t, recorder.Body.String(), "render failed")
core.AssertEqual(t, 1, renderer.calls)
}

func TestHttp_Handler_Ugly(t *testing.T) {
assetsDir := t.TempDir()
writeResult := core.WriteFile(core.PathJoin(assetsDir, "app.js"), []byte("asset();"), 0o600)
core.RequireTrue(t, writeResult.OK, writeResult.Error())
renderer := &renderStub{output: []byte("<main>unused</main>")}
handler := Handler(renderer, assetsDir, WithEntry("server.ts"))
recorder := httptest.NewRecorder()
request := httptest.NewRequest(nethttp.MethodGet, "http://example.test/app.js", nil)

handler.ServeHTTP(recorder, request)

core.AssertEqual(t, nethttp.StatusOK, recorder.Code)
core.AssertEqual(t, "asset();", recorder.Body.String())
core.AssertEqual(t, 0, renderer.calls)
}

func TestHttp_WithEntry_Good(t *testing.T) {
renderer := &renderStub{output: []byte("ok")}
handler := Handler(renderer, "", WithEntry("main.server.ts"))
recorder := httptest.NewRecorder()
request := httptest.NewRequest(nethttp.MethodGet, "http://example.test/", nil)

handler.ServeHTTP(recorder, request)

core.AssertEqual(t, nethttp.StatusOK, recorder.Code)
core.AssertEqual(t, "main.server.ts", renderer.entry)
core.AssertEqual(t, 1, renderer.calls)
}

func TestHttp_WithEntry_Bad(t *testing.T) {
renderer := &renderStub{output: []byte("unused")}
handler := Handler(renderer, "", WithEntry(" "))
recorder := httptest.NewRecorder()
request := httptest.NewRequest(nethttp.MethodGet, "http://example.test/", nil)

handler.ServeHTTP(recorder, request)

core.AssertEqual(t, nethttp.StatusInternalServerError, recorder.Code)
core.AssertContains(t, recorder.Body.String(), "entry unavailable")
core.AssertEqual(t, 0, renderer.calls)
}

func TestHttp_WithEntry_Ugly(t *testing.T) {
renderer := &renderStub{output: []byte("last wins")}
handler := Handler(renderer, "", WithEntry("first.ts"), WithEntry("second.ts"))
recorder := httptest.NewRecorder()
request := httptest.NewRequest(nethttp.MethodGet, "http://example.test/", nil)

handler.ServeHTTP(recorder, request)

core.AssertEqual(t, nethttp.StatusOK, recorder.Code)
core.AssertEqual(t, "second.ts", renderer.entry)
core.AssertEqual(t, "last wins", recorder.Body.String())
}
6 changes: 6 additions & 0 deletions go/display/pipe/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//go:build !js

// SPDX-Licence-Identifier: EUPL-1.2

// Package pipe emits one server-rendered TypeScript result to a writer.
package pipe
34 changes: 34 additions & 0 deletions go/display/pipe/pipe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//go:build !js

// SPDX-Licence-Identifier: EUPL-1.2

package pipe

import (
core "dappco.re/go"
tsengine "dappco.re/go/html/engine/ts"
)

// Emit renders entry once with a nil input and writes the exact output bytes
// to writer.
func Emit(engine tsengine.Renderer, entry string, writer core.Writer) error {
if engine == nil {
return core.E("pipe.Emit", "render engine is nil", nil)

Check failure on line 16 in go/display/pipe/pipe.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "pipe.Emit" 5 times.

See more on https://sonarcloud.io/project/issues?id=dAppCore_go-html&issues=AZ-Pr_uYvU850_1woTNq&open=AZ-Pr_uYvU850_1woTNq&pullRequest=33
}
if writer == nil {
return core.E("pipe.Emit", "writer is nil", nil)
}

output, err := engine.Render(core.Background(), entry, nil)
if err != nil {
return core.E("pipe.Emit", "render TypeScript entry", err)
}
written, err := writer.Write(output)
if err != nil {
return core.E("pipe.Emit", "write rendered output", err)
}
if written != len(output) {
return core.E("pipe.Emit", "short write", nil)
}
return nil
}
9 changes: 9 additions & 0 deletions go/display/pipe/pipe_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build !js

// SPDX-Licence-Identifier: EUPL-1.2

package pipe

func ExampleEmit() {
_ = Emit
}
65 changes: 65 additions & 0 deletions go/display/pipe/pipe_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//go:build !js

// SPDX-Licence-Identifier: EUPL-1.2

package pipe

import (
"testing"

core "dappco.re/go"
)

type pipeRenderer struct {
output []byte
err error
entry string
input any
}

func (r *pipeRenderer) Render(_ core.Context, entry string, input any) ([]byte, error) {
r.entry = entry
r.input = input
return r.output, r.err
}

type shortWriter struct{}

func (shortWriter) Write(data []byte) (int, error) {
if len(data) == 0 {
return 0, nil
}
return len(data) - 1, nil
}

func TestPipe_Emit_Good(t *testing.T) {
renderer := &pipeRenderer{output: []byte("<p>ready</p>")}
output := core.NewBuffer()
err := Emit(renderer, "server.ts", output)

core.AssertNoError(t, err)
core.AssertEqual(t, "<p>ready</p>", output.String())
core.AssertEqual(t, "server.ts", renderer.entry)
core.AssertNil(t, renderer.input)
}

func TestPipe_Emit_Bad(t *testing.T) {
renderer := &pipeRenderer{err: core.E("test.render", "failed", nil)}
output := core.NewBuffer()
err := Emit(renderer, "broken.ts", output)

core.AssertError(t, err)
core.AssertContains(t, err.Error(), "render TypeScript entry")
core.AssertEqual(t, 0, output.Len())
core.AssertEqual(t, "broken.ts", renderer.entry)
}

func TestPipe_Emit_Ugly(t *testing.T) {
renderer := &pipeRenderer{output: []byte("partial")}
err := Emit(renderer, "server.ts", shortWriter{})

core.AssertError(t, err)
core.AssertContains(t, err.Error(), "short write")
core.AssertEqual(t, "server.ts", renderer.entry)
core.AssertNil(t, renderer.input)
}
6 changes: 6 additions & 0 deletions go/display/socket/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//go:build !js

// SPDX-Licence-Identifier: EUPL-1.2

// Package socket serves TypeScript SSR over an existing TCP or Unix listener.
package socket
31 changes: 31 additions & 0 deletions go/display/socket/socket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//go:build !js

// SPDX-Licence-Identifier: EUPL-1.2

package socket

import (
"net"
nethttp "net/http"

core "dappco.re/go"
httpdisplay "dappco.re/go/html/display/http"
tsengine "dappco.re/go/html/engine/ts"
)

// Serve serves the HTTP SSR handler over listener until the listener closes.
// The listener may use TCP or a Unix-domain socket.
func Serve(engine tsengine.Renderer, listener net.Listener, opts ...httpdisplay.Option) error {
if engine == nil {
return core.E("socket.Serve", "render engine is nil", nil)

Check failure on line 20 in go/display/socket/socket.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "socket.Serve" 3 times.

See more on https://sonarcloud.io/project/issues?id=dAppCore_go-html&issues=AZ-Pr_uivU850_1woTNr&open=AZ-Pr_uivU850_1woTNr&pullRequest=33
}
if listener == nil {
return core.E("socket.Serve", "listener is nil", nil)
}

err := nethttp.Serve(listener, httpdisplay.Handler(engine, "", opts...))
if err == nil || core.Is(err, nethttp.ErrServerClosed) || core.Is(err, net.ErrClosed) {
return nil
}
return core.E("socket.Serve", "serve render listener", err)
}
9 changes: 9 additions & 0 deletions go/display/socket/socket_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build !js

// SPDX-Licence-Identifier: EUPL-1.2

package socket

func ExampleServe() {
_ = Serve
}
Loading
Loading