-
Notifications
You must be signed in to change notification settings - Fork 9
/
rendertask.go
158 lines (141 loc) · 5.26 KB
/
rendertask.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package go_ssr
import (
"fmt"
"github.com/buke/quickjs-go"
"github.com/natewong1313/go-react-ssr/internal/reactbuilder"
"github.com/rs/zerolog"
)
type renderTask struct {
engine *Engine
logger zerolog.Logger
routeID string
filePath string
props string
config RenderConfig
serverRenderResult chan serverRenderResult
clientRenderResult chan clientRenderResult
}
type serverRenderResult struct {
html string
css string
err error
}
type clientRenderResult struct {
js string
dependencies []string
err error
}
// Start starts the render task, returns the rendered html, css, and js for hydration
func (rt *renderTask) Start() (string, string, string, error) {
rt.serverRenderResult = make(chan serverRenderResult)
rt.clientRenderResult = make(chan clientRenderResult)
// Assigns the parent file to the routeID so that the cache can be invalidated when the parent file changes
rt.engine.CacheManager.SetParentFile(rt.routeID, rt.filePath)
// Render for server and client concurrently
go rt.doRender("server")
go rt.doRender("client")
// Wait for both to finish
srResult := <-rt.serverRenderResult
if srResult.err != nil {
rt.logger.Error().Err(srResult.err).Msg("Failed to build for server")
return "", "", "", srResult.err
}
crResult := <-rt.clientRenderResult
if crResult.err != nil {
rt.logger.Error().Err(crResult.err).Msg("Failed to build for client")
return "", "", "", crResult.err
}
// Set the parent file dependencies so that the cache can be invalidated a dependency changes
go rt.engine.CacheManager.SetParentFileDependencies(rt.filePath, crResult.dependencies)
return srResult.html, srResult.css, crResult.js, nil
}
func (rt *renderTask) doRender(buildType string) {
// Check if the build is in the cache
build, buildFound := rt.getBuildFromCache(buildType)
if !buildFound {
// Build the file if it's not in the cache
newBuild, err := rt.buildFile(buildType)
if err != nil {
rt.handleBuildError(err, buildType)
return
}
rt.updateBuildCache(newBuild, buildType)
build = newBuild
}
// JS is built without props so that the props can be injected into cached JS builds
js := injectProps(build.JS, rt.props)
if buildType == "server" {
// Then call that file with node to get the rendered HTML
renderedHTML, err := renderReactToHTMLNew(js)
rt.serverRenderResult <- serverRenderResult{html: renderedHTML, css: build.CSS, err: err}
} else {
rt.clientRenderResult <- clientRenderResult{js: js, dependencies: build.Dependencies}
}
}
// getBuild returns the build from the cache if it exists
func (rt *renderTask) getBuildFromCache(buildType string) (reactbuilder.BuildResult, bool) {
if buildType == "server" {
return rt.engine.CacheManager.GetServerBuild(rt.filePath)
} else {
return rt.engine.CacheManager.GetClientBuild(rt.filePath)
}
}
// buildFile gets the contents of the file to be built and builds it with reactbuilder
func (rt *renderTask) buildFile(buildType string) (reactbuilder.BuildResult, error) {
buildContents, err := rt.getBuildContents(buildType)
if err != nil {
return reactbuilder.BuildResult{}, err
}
if buildType == "server" {
return reactbuilder.BuildServer(buildContents, rt.engine.Config.FrontendDir, rt.engine.Config.AssetRoute)
} else {
return reactbuilder.BuildClient(buildContents, rt.engine.Config.FrontendDir, rt.engine.Config.AssetRoute)
}
}
// getBuildContents gets the required imports based on the config and returns the contents to be built with reactbuilder
func (rt *renderTask) getBuildContents(buildType string) (string, error) {
var imports []string
if rt.engine.CachedLayoutCSSFilePath != "" {
imports = append(imports, fmt.Sprintf(`import "%s";`, rt.engine.CachedLayoutCSSFilePath))
}
if rt.engine.Config.LayoutFilePath != "" {
imports = append(imports, fmt.Sprintf(`import Layout from "%s";`, rt.engine.Config.LayoutFilePath))
}
if buildType == "server" {
return reactbuilder.GenerateServerBuildContents(imports, rt.filePath, rt.engine.Config.LayoutFilePath != "")
} else {
return reactbuilder.GenerateClientBuildContents(imports, rt.filePath, rt.engine.Config.LayoutFilePath != "")
}
}
// handleBuildError handles the error from building the file and sends it to the appropriate channel
func (rt *renderTask) handleBuildError(err error, buildType string) {
if buildType == "server" {
rt.serverRenderResult <- serverRenderResult{err: err}
} else {
rt.clientRenderResult <- clientRenderResult{err: err}
}
}
// updateBuildCache updates the cache with the new build
func (rt *renderTask) updateBuildCache(build reactbuilder.BuildResult, buildType string) {
if buildType == "server" {
rt.engine.CacheManager.SetServerBuild(rt.filePath, build)
} else {
rt.engine.CacheManager.SetClientBuild(rt.filePath, build)
}
}
// injectProps injects the props into the already compiled JS
func injectProps(compiledJS, props string) string {
return fmt.Sprintf(`var props = %s; %s`, props, compiledJS)
}
// renderReactToHTML uses node to execute the server js file which outputs the rendered HTML
func renderReactToHTMLNew(js string) (string, error) {
rt := quickjs.NewRuntime()
defer rt.Close()
ctx := rt.NewContext()
defer ctx.Close()
res, err := ctx.Eval(js)
if err != nil {
return "", err
}
return res.String(), nil
}