├── .github ├── README.md └── workflows │ ├── release.yml │ └── run-tests.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── config.go ├── css.go ├── css_test.go ├── engine.go ├── engine_test.go ├── examples ├── echo │ ├── .dockerignore │ ├── Dockerfile │ ├── docker-compose.yml │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── main_test.go │ └── models │ │ └── props.go ├── fiber │ ├── .dockerignore │ ├── Dockerfile │ ├── docker-compose.yml │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── main_test.go │ └── models │ │ └── props.go ├── frontend-mui │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── go.png │ │ └── react.png │ ├── src │ │ ├── Home.css │ │ ├── Home.tsx │ │ ├── Layout.tsx │ │ ├── components │ │ │ └── Counter.tsx │ │ ├── generated.d.ts │ │ └── index.d.ts │ └── tsconfig.json ├── frontend-tailwind │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── go.png │ │ └── react.png │ ├── src │ │ ├── Home.tsx │ │ ├── Main.css │ │ ├── components │ │ │ └── Counter.tsx │ │ ├── generated.d.ts │ │ └── index.d.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── frontend │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── go.png │ │ └── react.png │ ├── src │ │ ├── Home.css │ │ ├── Home.tsx │ │ ├── Layout.tsx │ │ ├── components │ │ │ ├── Counter.module.css │ │ │ └── Counter.tsx │ │ ├── generated.d.ts │ │ └── index.d.ts │ └── tsconfig.json └── gin │ ├── .dockerignore │ ├── Dockerfile │ ├── docker-compose.yml │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── main_test.go │ └── models │ └── props.go ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── gossr-cli ├── cmd │ ├── create │ │ ├── bootstrapper.go │ │ ├── create.go │ │ ├── prompts.go │ │ └── temp_dir.go │ ├── root.go │ └── update │ │ ├── check_update.go │ │ ├── latest_version.go │ │ ├── update.go │ │ └── version_file.go ├── go.mod ├── go.sum ├── logger │ └── logger.go ├── main.go └── utils │ ├── error.go │ └── path.go ├── hotreload.go ├── internal ├── cache │ └── manager.go ├── html │ ├── base_template.go │ ├── error_template.go │ └── render_html.go ├── reactbuilder │ ├── build.go │ └── contents.go ├── typeconverter │ ├── create_temp_file.go │ ├── get_structs.go │ ├── models.go │ └── start.go └── utils │ ├── cachedir.go │ └── filepath.go ├── render.go └── rendertask.go /.github/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | --- 7 | 8 |

9 | Go Report 10 | GoDoc 11 | MIT License 12 |

13 | 14 | Go-SSR is a drop in plugin to **any** existing Go web framework to allow **server rendering** [React](https://react.dev/). It's powered by [esbuild](https://esbuild.github.io/) and allows for passing props from Go to React with **type safety**. 15 | 16 | 18 | 19 | Go-SSR was developed due to a lack of an existing product in the Go ecosystem that made it easy to build full-stack React apps. At the time, most Go web app projects were either built with a static React frontend with lots of client-side logic or html templates. I envisioned creating a new solution that would allow you to create full-stack Go apps with React but with logic being moved to the server and being able to pass that logic down with type-safe props. This project was inspired by [Remix](https://remix.run/) and [Next.JS](https://nextjs.org/), but aims to be a plugin and not a framework. 20 | 21 | # 📜 Features 22 | 23 | - Lightning fast compiling 24 | - Auto generated Typescript structs for props 25 | - Hot reloading 26 | - Simple error reporting 27 | - Production optimized 28 | - Drop in to any existing Go web server 29 | 30 | 31 | 32 | # 🛠️ Getting Started 33 | 34 | Go-SSR was designed with the idea of being dead simple to install. Below are 2 easy ways of setting it up: 35 | 36 | ## ⚡️ Using the CLI tool 37 | 38 | 39 | 40 | The easiest way to get a project up and running is by using the command line tool. Install it with the following command 41 | 42 | ```console 43 | $ go install github.com/natewong1313/go-react-ssr/gossr-cli@latest 44 | ``` 45 | 46 | Then you can call the following command to create a project 47 | 48 | ```console 49 | $ gossr-cli create 50 | ``` 51 | 52 | You'll be prompted the path to place the project, what web framework you want to use, and whether or not you want to use Tailwind 53 | 54 | ## 📝 Add to existing web server 55 | 56 | To add Go-SSR to an existing Go web server, take a look at the [examples](/examples) folder to get an idea of what a project looks like. In general, you'll want to follow these commands: 57 | 58 | ```console 59 | $ go get -u github.com/natewong1313/go-react-ssr 60 | ``` 61 | 62 | Then, add imports into your main file 63 | 64 | ```go 65 | import ( 66 | ... 67 | gossr "github.com/natewong1313/go-react-ssr" 68 | ) 69 | ``` 70 | 71 | In your main function, initialize the plugin. Create a folder for your structs that hold your props to go, which is called `models` in the below example. You'll also want to create a folder for your React code (called `frontend` in this example) inside your project and specifiy the paths in the config. You may want to clone the [example folder](/examples/frontend/) and use that. 72 | 73 | ```go 74 | engine, err := gossr.New(gossr.Config{ 75 | AssetRoute: "/assets", 76 | FrontendDir: "./frontend/src", 77 | GeneratedTypesPath: "./frontend/src/generated.d.ts", 78 | PropsStructsPath: "./models/props.go", 79 | }) 80 | ``` 81 | 82 | Once the plugin has been initialized, you can call the `engine.RenderRoute` function to compile your React file to a string 83 | 84 | ```go 85 | g.GET("/", func(c *gin.Context) { 86 | renderedResponse := engine.RenderRoute(gossr.RenderConfig{ 87 | File: "Home.tsx", 88 | Title: "Example app", 89 | MetaTags: map[string]string{ 90 | "og:title": "Example app", 91 | "description": "Hello world!", 92 | }, 93 | Props: &models.IndexRouteProps{ 94 | InitialCount: rand.Intn(100), 95 | }, 96 | }) 97 | c.Writer.Write(renderedResponse) 98 | }) 99 | ``` 100 | 101 | # 🚀 Deploying to production 102 | 103 | All of the examples come with a Dockerfile that you can use to deploy to production. You can also use the [gossr-cli](#-using-the-cli-tool) to create a project with a Dockerfile. 104 | Below is an example Dockerfile 105 | 106 | ```Dockerfile 107 | FROM golang:1.21-alpine as build-backend 108 | RUN apk add git 109 | ADD .. /build 110 | WORKDIR /build 111 | 112 | RUN go mod download 113 | RUN go get -u github.com/natewong1313/go-react-ssr 114 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-w -X main.APP_ENV=production" -a -o main 115 | 116 | 117 | FROM node:16-alpine as build-frontend 118 | 119 | ADD ./frontend /frontend 120 | WORKDIR /frontend 121 | 122 | RUN npm install 123 | 124 | # if tailwind is enabled, use "FROM node:16-alpine" instead 125 | FROM alpine:latest 126 | COPY --from=build-backend /build/main ./app/main 127 | COPY --from=build-frontend /frontend ./app/frontend 128 | 129 | WORKDIR /app 130 | RUN chmod +x ./main 131 | EXPOSE 8080 132 | CMD ["./main"] 133 | ``` 134 | 135 | Go SSR has been tested and deployed on the following platforms: 136 | 137 | - [Fly.io](https://fly.io/) - [example app](https://sparkling-smoke-7627.fly.dev/) 138 | - [Render](https://render.com/) - [example app](https://my-gossr-test.onrender.com/) 139 | - [Hop.io](https://hop.io/) - [example app](https://my-gossr-test.hop.sh/) 140 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Go project 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" # triggers only if push new tag version, like `0.8.4` or else 7 | jobs: 8 | build: 9 | name: GoReleaser build 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check out code into the Go module directory 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 # See: https://goreleaser.com/ci/actions/ 17 | 18 | - name: Set up Go 1.21 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: 1.21 22 | id: go 23 | 24 | # - name: Run GoReleaser 25 | # uses: goreleaser/goreleaser-action@master 26 | # with: 27 | # version: latest 28 | # args: release --rm-dist 29 | # env: 30 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Update godoc.org 33 | run: | 34 | OPERATOR_VERSION=$(git describe --tags) 35 | curl https://proxy.golang.org/github.com/natewong1313/go-react-ssr/@v/${OPERATOR_VERSION}.info 36 | curl -X POST https://pkg.go.dev/fetch/github.com/natewong1313/go-react-ssr@${OPERATOR_VERSION} 37 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run test suite 2 | on: 3 | push: 4 | branches: 5 | - "*" 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Setup Go 14 | uses: actions/setup-go@v4 15 | with: 16 | go-version: "1.21" 17 | - name: Install dependencies 18 | run: go get . 19 | - name: Build 20 | run: go build -v ./... 21 | - name: Test with the Go CLI 22 | run: go test -v ./... 23 | - name: Setup Node 24 | uses: actions/setup-node@v3 25 | - name: Install npm dependencies 26 | working-directory: ./examples/frontend 27 | run: npm install 28 | - name: Test Fiber example 29 | run: go test -v ./examples/fiber 30 | - name: Setup Node for tailwind example 31 | uses: actions/setup-node@v3 32 | - name: Install npm dependencies 33 | working-directory: ./examples/frontend-tailwind 34 | run: npm install 35 | - name: Test Gin example 36 | run: go test -v ./examples/gin 37 | - name: Setup Node for material ui example 38 | uses: actions/setup-node@v3 39 | - name: Install npm dependencies 40 | working-directory: ./examples/frontend-mui 41 | run: npm install 42 | - name: Test Echo example 43 | run: go test -v ./examples/echo 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .tmp 3 | tmp 4 | .DS_Store 5 | 6 | .idea 7 | 8 | generated.d.ts -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - skip: true 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nate Wong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package go_ssr 2 | 3 | import ( 4 | "fmt" 5 | "github.com/natewong1313/go-react-ssr/internal/utils" 6 | "os" 7 | "path" 8 | ) 9 | 10 | // Config is the config for starting the engine 11 | type Config struct { 12 | AppEnv string // "production" or "development" 13 | AssetRoute string // The route to serve assets from, e.g. "/assets" 14 | FrontendDir string // The path to the frontend folder, where your React app lives 15 | GeneratedTypesPath string // The path to the generated types file 16 | PropsStructsPath string // The path to the Go structs file, the structs will be generated to TS types 17 | LayoutFilePath string // The path to the layout file, relative to the frontend dir 18 | LayoutCSSFilePath string // The path to the layout css file, relative to the frontend dir 19 | TailwindConfigPath string // The path to the tailwind config file 20 | HotReloadServerPort int // The port to run the hot reload server on, 3001 by default 21 | } 22 | 23 | // Validate validates the config 24 | func (c *Config) Validate() error { 25 | if !checkPathExists(c.FrontendDir) { 26 | return fmt.Errorf("frontend dir ar %s does not exist", c.FrontendDir) 27 | } 28 | if os.Getenv("APP_ENV") != "production" && !checkPathExists(c.PropsStructsPath) { 29 | return fmt.Errorf("props structs path at %s does not exist", c.PropsStructsPath) 30 | } 31 | if c.LayoutFilePath != "" && !checkPathExists(path.Join(c.FrontendDir, c.LayoutFilePath)) { 32 | return fmt.Errorf("layout css file path at %s/%s does not exist", c.FrontendDir, c.LayoutCSSFilePath) 33 | } 34 | if c.LayoutCSSFilePath != "" && !checkPathExists(path.Join(c.FrontendDir, c.LayoutCSSFilePath)) { 35 | return fmt.Errorf("layout css file path at %s/%s does not exist", c.FrontendDir, c.LayoutCSSFilePath) 36 | } 37 | if c.TailwindConfigPath != "" && c.LayoutCSSFilePath == "" { 38 | return fmt.Errorf("layout css file path must be provided when using tailwind") 39 | } 40 | if c.HotReloadServerPort == 0 { 41 | c.HotReloadServerPort = 3001 42 | } 43 | c.setFilePaths() 44 | return nil 45 | } 46 | 47 | // setFilePaths sets any paths in the config to their absolute paths 48 | func (c *Config) setFilePaths() { 49 | c.FrontendDir = utils.GetFullFilePath(c.FrontendDir) 50 | c.GeneratedTypesPath = utils.GetFullFilePath(c.GeneratedTypesPath) 51 | c.PropsStructsPath = utils.GetFullFilePath(c.PropsStructsPath) 52 | if c.LayoutFilePath != "" { 53 | c.LayoutFilePath = path.Join(c.FrontendDir, c.LayoutFilePath) 54 | } 55 | if c.LayoutCSSFilePath != "" { 56 | c.LayoutCSSFilePath = path.Join(c.FrontendDir, c.LayoutCSSFilePath) 57 | } 58 | if c.TailwindConfigPath != "" { 59 | c.TailwindConfigPath = utils.GetFullFilePath(c.TailwindConfigPath) 60 | } 61 | } 62 | 63 | func checkPathExists(path string) bool { 64 | _, err := os.Stat(utils.GetFullFilePath(path)) 65 | return !os.IsNotExist(err) 66 | } 67 | -------------------------------------------------------------------------------- /css.go: -------------------------------------------------------------------------------- 1 | package go_ssr 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "runtime" 12 | 13 | "github.com/buger/jsonparser" 14 | "github.com/natewong1313/go-react-ssr/internal/utils" 15 | ) 16 | 17 | // BuildLayoutCSSFile builds the layout css file if it exists 18 | func (engine *Engine) BuildLayoutCSSFile() error { 19 | if engine.CachedLayoutCSSFilePath == "" && engine.Config.LayoutCSSFilePath != "" { 20 | layoutCSSCacheDir, err := utils.GetCSSCacheDir() 21 | if err != nil { 22 | return err 23 | } 24 | cachedCSSFilePath, err := createCachedCSSFile(layoutCSSCacheDir, engine.Config.LayoutCSSFilePath) 25 | if err != nil { 26 | return err 27 | } 28 | engine.CachedLayoutCSSFilePath = cachedCSSFilePath 29 | } 30 | if engine.Config.TailwindConfigPath != "" { 31 | engine.Logger.Debug().Msg("Building css file with tailwind") 32 | return engine.buildCSSWithTailwind() 33 | } 34 | return nil 35 | } 36 | 37 | // createCachedCSSFile creates a cached css file from the layout css file 38 | func createCachedCSSFile(layoutCSSCacheDir, layoutCSSFilePath string) (string, error) { 39 | cachedCSSFilePath := utils.GetFullFilePath(filepath.Join(layoutCSSCacheDir, "gossr.css")) 40 | file, err := os.Create(cachedCSSFilePath) 41 | if err != nil { 42 | return "", err 43 | } 44 | defer file.Close() 45 | globalCSSFile, err := os.Open(layoutCSSFilePath) 46 | if err != nil { 47 | return "", err 48 | } 49 | defer globalCSSFile.Close() 50 | _, err = io.Copy(file, globalCSSFile) 51 | return cachedCSSFilePath, err 52 | } 53 | 54 | // buildCSSWithTailwind builds the css file with tailwind cli 55 | func (engine *Engine) buildCSSWithTailwind() error { 56 | cmd := exec.Command("npx", "tailwindcss", "-i", engine.Config.LayoutCSSFilePath, "-o", engine.CachedLayoutCSSFilePath) 57 | // if in production, use the standalone tailwind executable instead of node 58 | if os.Getenv("APP_ENV") == "production" { 59 | executableName, err := detectTailwindDownloadName() 60 | if err != nil { 61 | return err 62 | } 63 | executableDir, err := utils.GetTailwindExecutableDir() 64 | if err != nil { 65 | return err 66 | } 67 | executablePath := filepath.Join(executableDir, executableName) 68 | // check if the executable has already been installed 69 | if _, err := os.Stat(executablePath); os.IsNotExist(err) { 70 | engine.Logger.Debug().Msgf("Downloading tailwind executable to %s", executablePath) 71 | if err = engine.downloadTailwindExecutable(executableName, executableDir); err != nil { 72 | return err 73 | } 74 | } 75 | cmd = exec.Command(executablePath, "-i", engine.Config.LayoutCSSFilePath, "-o", engine.CachedLayoutCSSFilePath) 76 | } 77 | // Set the working directory to the directory of the tailwind config file 78 | cmd.Dir = filepath.Dir(engine.Config.TailwindConfigPath) 79 | _, err := cmd.CombinedOutput() 80 | return err 81 | } 82 | 83 | // downloadTailwindExecutable downloads the tailwind executable from github releases (https://tailwindcss.com/blog/standalone-cli) 84 | func (engine *Engine) downloadTailwindExecutable(executableName string, executableDir string) error { 85 | version, err := getLatestTailwindVersion() 86 | if err != nil { 87 | return err 88 | } 89 | 90 | file, err := os.Create(filepath.Join(executableDir, executableName)) 91 | if err != nil { 92 | return err 93 | } 94 | defer file.Close() 95 | 96 | resp, err := http.Get(fmt.Sprintf("https://github.com/tailwindlabs/tailwindcss/releases/download/%s/%s", version, executableName)) 97 | if err != nil { 98 | return err 99 | } 100 | defer resp.Body.Close() 101 | 102 | _, err = io.Copy(file, resp.Body) 103 | if err != nil { 104 | return err 105 | } 106 | return nil 107 | } 108 | 109 | // getLatestTailwindVersion gets the latest tailwind release version from the github api 110 | func getLatestTailwindVersion() (string, error) { 111 | resp, err := http.Get("https://api.github.com/repos/tailwindlabs/tailwindcss/releases/latest") 112 | if err != nil { 113 | return "", err 114 | } 115 | defer resp.Body.Close() 116 | 117 | respBody, err := io.ReadAll(resp.Body) 118 | if err != nil { 119 | return "", err 120 | } 121 | version, err := jsonparser.GetString(respBody, "name") 122 | return version, err 123 | } 124 | 125 | // detectTailwindDownloadName detects the tailwind executable download name based on the OS and architecture 126 | func detectTailwindDownloadName() (string, error) { 127 | os := runtime.GOOS 128 | arch := runtime.GOARCH 129 | switch os { 130 | case "darwin": 131 | switch arch { 132 | case "arm64": 133 | return "tailwindcss-macos-arm64", nil 134 | case "amd64": 135 | return "tailwindcss-macos-x64", nil 136 | } 137 | case "linux": 138 | switch arch { 139 | case "arm64": 140 | return "tailwindcss-linux-arm64", nil 141 | case "arm": 142 | return "tailwindcss-linux-armv7", nil 143 | case "amd64": 144 | return "tailwindcss-linux-x64", nil 145 | } 146 | case "windows": 147 | switch arch { 148 | case "arm64": 149 | return "tailwindcss-windows-arm64.exe", nil 150 | case "amd64": 151 | return "tailwindcss-windows-x64.exe", nil 152 | } 153 | } 154 | return "", errors.New("unsupported OS/Architecture") 155 | } 156 | -------------------------------------------------------------------------------- /css_test.go: -------------------------------------------------------------------------------- 1 | package go_ssr 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/rs/zerolog" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestEngine_BuildLayoutCSSFile(t *testing.T) { 12 | type test struct { 13 | name string 14 | shouldContain string 15 | config *Config 16 | } 17 | originalContents, err := os.ReadFile("./examples/frontend/src/Home.css") 18 | assert.Nil(t, err, "ReadFile should not return an error") 19 | tests := []test{ 20 | { 21 | name: "should clone layout css file", 22 | shouldContain: string(originalContents), 23 | config: &Config{ 24 | AppEnv: "production", 25 | FrontendDir: "./examples/frontend/src", 26 | LayoutCSSFilePath: "Home.css", 27 | }, 28 | }, 29 | { 30 | name: "should build layout css file with tailwind", 31 | shouldContain: "tailwindcss", 32 | config: &Config{ 33 | AppEnv: "production", 34 | FrontendDir: "./examples/frontend-tailwind/src", 35 | LayoutCSSFilePath: "Main.css", 36 | TailwindConfigPath: "./examples/frontend-tailwind/tailwind.config.js", 37 | }, 38 | }, 39 | } 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | tt.config.setFilePaths() 43 | engine := &Engine{ 44 | Logger: zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger(), 45 | Config: tt.config, 46 | } 47 | err = engine.BuildLayoutCSSFile() 48 | assert.Nil(t, err, "BuildLayoutCSSFile should not return an error, got %v", err) 49 | assert.NotNilf(t, engine.CachedLayoutCSSFilePath, "CachedLayoutCSSFilePath should not be nil") 50 | contents, err := os.ReadFile(engine.CachedLayoutCSSFilePath) 51 | assert.Nil(t, err, "ReadFile should not return an error, got %v", err) 52 | assert.Contains(t, string(contents), tt.shouldContain) 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /engine.go: -------------------------------------------------------------------------------- 1 | package go_ssr 2 | 3 | import ( 4 | "github.com/natewong1313/go-react-ssr/internal/cache" 5 | "github.com/natewong1313/go-react-ssr/internal/typeconverter" 6 | "github.com/natewong1313/go-react-ssr/internal/utils" 7 | "github.com/rs/zerolog" 8 | "os" 9 | ) 10 | 11 | type Engine struct { 12 | Logger zerolog.Logger 13 | Config *Config 14 | HotReload *HotReload 15 | CacheManager *cache.Manager 16 | CachedLayoutCSSFilePath string 17 | } 18 | 19 | // New creates a new gossr Engine instance 20 | func New(config Config) (*Engine, error) { 21 | engine := &Engine{ 22 | Logger: zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger(), 23 | Config: &config, 24 | CacheManager: cache.NewManager(), 25 | } 26 | if err := os.Setenv("APP_ENV", config.AppEnv); err != nil { 27 | engine.Logger.Err(err).Msg("Failed to set APP_ENV environment variable") 28 | } 29 | err := config.Validate() 30 | if err != nil { 31 | engine.Logger.Err(err).Msg("Failed to validate config") 32 | return nil, err 33 | } 34 | utils.CleanCacheDirectories() 35 | // If using a layout css file, build it and cache it 36 | if config.LayoutCSSFilePath != "" { 37 | if err = engine.BuildLayoutCSSFile(); err != nil { 38 | engine.Logger.Err(err).Msg("Failed to build layout css file") 39 | return nil, err 40 | } 41 | } 42 | 43 | // If running in production mode, return and don't start hot reload or type converter 44 | if os.Getenv("APP_ENV") == "production" { 45 | engine.Logger.Info().Msg("Running go-ssr in production mode") 46 | return engine, nil 47 | } 48 | engine.Logger.Info().Msg("Running go-ssr in development mode") 49 | engine.Logger.Debug().Msg("Starting type converter") 50 | // Start the type converter to convert Go types to Typescript types 51 | if err := typeconverter.Start(engine.Config.PropsStructsPath, engine.Config.GeneratedTypesPath); err != nil { 52 | engine.Logger.Err(err).Msg("Failed to init type converter") 53 | return nil, err 54 | } 55 | 56 | engine.Logger.Debug().Msg("Starting hot reload server") 57 | engine.HotReload = newHotReload(engine) 58 | engine.HotReload.Start() 59 | return engine, nil 60 | } 61 | -------------------------------------------------------------------------------- /engine_test.go: -------------------------------------------------------------------------------- 1 | package go_ssr 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "net" 7 | "os" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestNew_Dev(t *testing.T) { 13 | config := Config{ 14 | AppEnv: "development", 15 | FrontendDir: "./examples/frontend-tailwind/src", 16 | GeneratedTypesPath: "./examples/frontend-tailwind/src/generated.d.ts", 17 | PropsStructsPath: "./examples/gin/models/props.go", 18 | HotReloadServerPort: 4001, 19 | } 20 | 21 | originalContents, err := os.ReadFile(config.GeneratedTypesPath) 22 | assert.Nil(t, err, "ReadFile should not return an error") 23 | err = os.Truncate(config.GeneratedTypesPath, 0) 24 | assert.Nil(t, err, "os.Truncate should not return an error, got %v", err) 25 | _, err = New(config) 26 | assert.Nil(t, err, "gossr.New should not return an error, got %v", err) 27 | 28 | contents, err := os.ReadFile(config.GeneratedTypesPath) 29 | assert.Nil(t, err, "ReadFile should not return an error") 30 | assert.Contains(t, string(contents), "Do not change, this code is generated from Golang structs", "Types file should have generated code in it") 31 | 32 | var conn net.Conn 33 | for i := 1; i <= 3; i++ { 34 | conn, _ = net.DialTimeout("tcp", net.JoinHostPort("", fmt.Sprintf("%d", config.HotReloadServerPort)), time.Second) 35 | if conn != nil { 36 | conn.Close() 37 | break 38 | } 39 | } 40 | assert.NotNil(t, conn, "Hot reload server should be running on port %d", config.HotReloadServerPort) 41 | 42 | err = os.WriteFile(config.GeneratedTypesPath, originalContents, 0644) 43 | } 44 | 45 | func TestNew_Prod(t *testing.T) { 46 | config := Config{ 47 | AppEnv: "production", 48 | FrontendDir: "./examples/frontend/src", 49 | GeneratedTypesPath: "./examples/frontend/src/generated.d.ts", 50 | PropsStructsPath: "./examples/gin/models/props.go", 51 | } 52 | 53 | originalContents, err := os.ReadFile(config.GeneratedTypesPath) 54 | assert.Nil(t, err, "ReadFile should not return an error") 55 | err = os.Truncate(config.GeneratedTypesPath, 0) 56 | assert.Nil(t, err, "os.Truncate should not return an error, got %v", err) 57 | _, err = New(config) 58 | assert.Nil(t, err, "gossr.New should not return an error, got %v", err) 59 | 60 | contents, err := os.ReadFile(config.GeneratedTypesPath) 61 | assert.Nil(t, err, "ReadFile should not return an error") 62 | assert.Equal(t, string(contents), "", "Generated types file should be empty") 63 | 64 | err = os.WriteFile(config.GeneratedTypesPath, originalContents, 0644) 65 | } 66 | -------------------------------------------------------------------------------- /examples/echo/.dockerignore: -------------------------------------------------------------------------------- 1 | */node_modules 2 | .dockerignore 3 | Dockerfile 4 | tmp -------------------------------------------------------------------------------- /examples/echo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine as build-frontend 2 | ADD ./frontend-mui ./build 3 | WORKDIR /build 4 | RUN npm install 5 | 6 | FROM golang:1.21-alpine 7 | RUN apk add git 8 | ADD . /app 9 | COPY --from=build-frontend /build ./app/frontend-mui 10 | WORKDIR /app 11 | 12 | RUN go mod download 13 | RUN go get -u github.com/natewong1313/go-react-ssr 14 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-w -X main.APP_ENV=production" -a -o main 15 | 16 | RUN chmod +x ./main 17 | EXPOSE 8080 18 | CMD ["./main"] 19 | 20 | -------------------------------------------------------------------------------- /examples/echo/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "1" 2 | services: 3 | app: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | ports: 8 | - "8080:8080" 9 | environment: 10 | - APP_ENV=production 11 | -------------------------------------------------------------------------------- /examples/echo/go.mod: -------------------------------------------------------------------------------- 1 | module example.com/echo 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/labstack/echo/v4 v4.11.1 7 | github.com/natewong1313/go-react-ssr v0.1.0 8 | ) 9 | 10 | require ( 11 | github.com/buger/jsonparser v1.1.1 // indirect 12 | github.com/creasty/defaults v1.7.0 // indirect 13 | github.com/dlclark/regexp2 v1.7.0 // indirect 14 | github.com/dop251/goja v0.0.0-20230919151941-fc55792775de // indirect 15 | github.com/evanw/esbuild v0.19.3 // indirect 16 | github.com/fsnotify/fsnotify v1.6.0 // indirect 17 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect 18 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 19 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect 20 | github.com/gorilla/websocket v1.5.0 // indirect 21 | github.com/labstack/gommon v0.4.0 // indirect 22 | github.com/mattn/go-colorable v0.1.13 // indirect 23 | github.com/mattn/go-isatty v0.0.19 // indirect 24 | github.com/rs/zerolog v1.30.0 // indirect 25 | github.com/tkrajina/go-reflector v0.5.5 // indirect 26 | github.com/tkrajina/typescriptify-golang-structs v0.1.10 // indirect 27 | github.com/valyala/bytebufferpool v1.0.0 // indirect 28 | github.com/valyala/fasttemplate v1.2.2 // indirect 29 | golang.org/x/crypto v0.11.0 // indirect 30 | golang.org/x/net v0.12.0 // indirect 31 | golang.org/x/sys v0.12.0 // indirect 32 | golang.org/x/text v0.11.0 // indirect 33 | golang.org/x/time v0.3.0 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /examples/echo/go.sum: -------------------------------------------------------------------------------- 1 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 2 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 3 | github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= 4 | github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= 5 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 6 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= 9 | github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 14 | github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= 15 | github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 16 | github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= 17 | github.com/dop251/goja v0.0.0-20230919151941-fc55792775de h1:lA38Xtzr1Wo+iQdkN2E11ziKXJYRxLlzK/e2/fdxoEI= 18 | github.com/dop251/goja v0.0.0-20230919151941-fc55792775de/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= 19 | github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= 20 | github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= 21 | github.com/evanw/esbuild v0.19.3 h1:foPr0xwQM3lBWKBtscauTN9FrmJzRDVI2+EGOs82H/I= 22 | github.com/evanw/esbuild v0.19.3/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk= 23 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 24 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 25 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= 26 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 27 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 28 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 29 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 30 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= 31 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= 32 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 33 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 34 | github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= 35 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 36 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 37 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 38 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 39 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 40 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 41 | github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= 42 | github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= 43 | github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= 44 | github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 45 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 46 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 47 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 48 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 49 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 50 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 51 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 52 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 53 | github.com/natewong1313/go-react-ssr v0.1.0 h1:v/8q4dHGkyWXumrSIYZlZf/03DdG0RSMtQ1VcF1Ba5k= 54 | github.com/natewong1313/go-react-ssr v0.1.0/go.mod h1:48ue0an62Sqbk9Xgl2HiD9AA/lNJ7Wbz9mFhZHs9JYA= 55 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 56 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 57 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 58 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 59 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 60 | github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= 61 | github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= 62 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 63 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 64 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 65 | github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ= 66 | github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= 67 | github.com/tkrajina/typescriptify-golang-structs v0.1.10 h1:W/Ta9Kqo2lV+7bVXuQoUhZ0bDlnjwtPpKsy3A9M1nYg= 68 | github.com/tkrajina/typescriptify-golang-structs v0.1.10/go.mod h1:sjU00nti/PMEOZb07KljFlR+lJ+RotsC0GBQMv9EKls= 69 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 70 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 71 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 72 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 73 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 74 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 75 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 76 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 77 | golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= 78 | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= 79 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 80 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 81 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 82 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 83 | golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= 84 | golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 85 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 86 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 87 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 88 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 90 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 91 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 92 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 93 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 94 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 95 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 96 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 97 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 98 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 99 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 100 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 101 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 102 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 103 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 104 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 105 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 106 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 107 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 108 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 109 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= 110 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 111 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 112 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 113 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 114 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 115 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 116 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 117 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 118 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 119 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 120 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 121 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 122 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 123 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 124 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 125 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 126 | -------------------------------------------------------------------------------- /examples/echo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "example.com/echo/models" 5 | "github.com/labstack/echo/v4" 6 | "github.com/labstack/echo/v4/middleware" 7 | gossr "github.com/natewong1313/go-react-ssr" 8 | "log" 9 | "math/rand" 10 | "net/http" 11 | ) 12 | 13 | var APP_ENV string 14 | 15 | func main() { 16 | e := echo.New() 17 | e.Use(middleware.Recover()) 18 | e.Static("/assets", "../frontend-mui/public/") 19 | 20 | engine, err := gossr.New(gossr.Config{ 21 | AppEnv: APP_ENV, 22 | AssetRoute: "/assets", 23 | FrontendDir: "../frontend-mui/src", 24 | GeneratedTypesPath: "../frontend-mui/src/generated.d.ts", 25 | PropsStructsPath: "./models/props.go", 26 | LayoutFilePath: "Layout.tsx", 27 | }) 28 | if err != nil { 29 | log.Fatal("Failed to init go-react-ssr") 30 | } 31 | 32 | e.GET("/", func(c echo.Context) error { 33 | response := engine.RenderRoute(gossr.RenderConfig{ 34 | File: "Home.tsx", 35 | Title: "Echo example app", 36 | MetaTags: map[string]string{ 37 | "og:title": "Echo example app", 38 | "description": "Hello world!", 39 | }, 40 | Props: &models.IndexRouteProps{ 41 | InitialCount: rand.Intn(100), 42 | }, 43 | }) 44 | return c.HTML(http.StatusOK, string(response)) 45 | }) 46 | 47 | e.Start(":8080") 48 | } 49 | -------------------------------------------------------------------------------- /examples/echo/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_main(t *testing.T) { 14 | go main() 15 | // Wait for server to start 16 | for { 17 | conn, _ := net.DialTimeout("tcp", net.JoinHostPort("", "8080"), time.Millisecond*1000) 18 | if conn != nil { 19 | conn.Close() 20 | break 21 | } 22 | } 23 | res, err := http.Get("http://localhost:8080") 24 | assert.Nil(t, err) 25 | assert.Equal(t, 200, res.StatusCode) 26 | 27 | resBody, err := io.ReadAll(res.Body) 28 | assert.Nil(t, err) 29 | assert.NotNil(t, resBody) 30 | assert.NotContains(t, string(resBody), "An error occured!") 31 | } 32 | -------------------------------------------------------------------------------- /examples/echo/models/props.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type IndexRouteProps struct { 4 | InitialCount int `json:"initialCount"` 5 | } 6 | -------------------------------------------------------------------------------- /examples/fiber/.dockerignore: -------------------------------------------------------------------------------- 1 | */node_modules 2 | .dockerignore 3 | Dockerfile 4 | tmp -------------------------------------------------------------------------------- /examples/fiber/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine as build-frontend 2 | ADD ./frontend ./build 3 | WORKDIR /build 4 | RUN npm install 5 | 6 | FROM golang:1.21-alpine 7 | RUN apk add git 8 | ADD . /app 9 | COPY --from=build-frontend /build ./app/frontend 10 | WORKDIR /app 11 | 12 | RUN go mod download 13 | RUN go get -u github.com/natewong1313/go-react-ssr 14 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-w -X main.APP_ENV=production" -a -o main 15 | 16 | RUN chmod +x ./main 17 | EXPOSE 8080 18 | CMD ["./main"] 19 | 20 | -------------------------------------------------------------------------------- /examples/fiber/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "1" 2 | services: 3 | app: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | ports: 8 | - "8080:8080" 9 | environment: 10 | - APP_ENV=production 11 | -------------------------------------------------------------------------------- /examples/fiber/go.mod: -------------------------------------------------------------------------------- 1 | module example.com/fiber 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/gofiber/fiber/v2 v2.49.2 7 | github.com/natewong1313/go-react-ssr v0.0.693 8 | github.com/stretchr/testify v1.8.3 9 | ) 10 | 11 | require ( 12 | github.com/andybalholm/brotli v1.0.5 // indirect 13 | github.com/buger/jsonparser v1.1.1 // indirect 14 | github.com/creasty/defaults v1.7.0 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/evanw/esbuild v0.19.3 // indirect 17 | github.com/fsnotify/fsnotify v1.6.0 // indirect 18 | github.com/google/uuid v1.3.1 // indirect 19 | github.com/gorilla/websocket v1.5.0 // indirect 20 | github.com/joho/godotenv v1.5.1 // indirect 21 | github.com/klauspost/compress v1.17.0 // indirect 22 | github.com/mattn/go-colorable v0.1.13 // indirect 23 | github.com/mattn/go-isatty v0.0.19 // indirect 24 | github.com/mattn/go-runewidth v0.0.15 // indirect 25 | github.com/pmezard/go-difflib v1.0.0 // indirect 26 | github.com/rivo/uniseg v0.4.4 // indirect 27 | github.com/rs/zerolog v1.31.0 // indirect 28 | github.com/tkrajina/go-reflector v0.5.6 // indirect 29 | github.com/tkrajina/typescriptify-golang-structs v0.1.10 // indirect 30 | github.com/valyala/bytebufferpool v1.0.0 // indirect 31 | github.com/valyala/fasthttp v1.50.0 // indirect 32 | github.com/valyala/tcplisten v1.0.0 // indirect 33 | golang.org/x/sys v0.12.0 // indirect 34 | gopkg.in/yaml.v3 v3.0.1 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /examples/fiber/go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 2 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 4 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 5 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 6 | github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= 7 | github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/evanw/esbuild v0.19.3 h1:foPr0xwQM3lBWKBtscauTN9FrmJzRDVI2+EGOs82H/I= 12 | github.com/evanw/esbuild v0.19.3/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk= 13 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 14 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 15 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 16 | github.com/gofiber/fiber/v2 v2.49.2 h1:ONEN3/Vc+dUCxxDgZZwpqvhISgHqb+bu+isBiEyKEQs= 17 | github.com/gofiber/fiber/v2 v2.49.2/go.mod h1:gNsKnyrmfEWFpJxQAV0qvW6l70K1dZGno12oLtukcts= 18 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 19 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 20 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 21 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 22 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 23 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 24 | github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= 25 | github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 26 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 27 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 28 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 29 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 30 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 31 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 32 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 33 | github.com/natewong1313/go-react-ssr v0.0.693 h1:eDTrK+wlDnodv48HVFKH/r+NixdLAe+zaYwHp+SGSw4= 34 | github.com/natewong1313/go-react-ssr v0.0.693/go.mod h1:x1jsB3lxKxlprkOx51VKCiF+JE01oyvahqIrjo3kQRc= 35 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 36 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 37 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 38 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 39 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 40 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 41 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 42 | github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= 43 | github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 46 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 47 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 48 | github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= 49 | github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE= 50 | github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= 51 | github.com/tkrajina/typescriptify-golang-structs v0.1.10 h1:W/Ta9Kqo2lV+7bVXuQoUhZ0bDlnjwtPpKsy3A9M1nYg= 52 | github.com/tkrajina/typescriptify-golang-structs v0.1.10/go.mod h1:sjU00nti/PMEOZb07KljFlR+lJ+RotsC0GBQMv9EKls= 53 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 54 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 55 | github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= 56 | github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= 57 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 58 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 59 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 64 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 66 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 67 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 68 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 69 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 70 | -------------------------------------------------------------------------------- /examples/fiber/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | 7 | "example.com/fiber/models" 8 | "github.com/gofiber/fiber/v2" 9 | "github.com/gofiber/fiber/v2/middleware/favicon" 10 | "github.com/gofiber/fiber/v2/middleware/logger" 11 | gossr "github.com/natewong1313/go-react-ssr" 12 | ) 13 | 14 | var APP_ENV string 15 | 16 | func main() { 17 | app := fiber.New() 18 | app.Use(logger.New()) 19 | app.Use(favicon.New(favicon.Config{ 20 | File: "../frontend/public/favicon.ico", 21 | URL: "/favicon.ico", 22 | })) 23 | app.Static("/assets", "../frontend/public/") 24 | 25 | engine, err := gossr.New(gossr.Config{ 26 | AppEnv: APP_ENV, 27 | AssetRoute: "/assets", 28 | FrontendDir: "../frontend/src", 29 | GeneratedTypesPath: "../frontend/src/generated.d.ts", 30 | PropsStructsPath: "./models/props.go", 31 | }) 32 | if err != nil { 33 | log.Fatal("Failed to init go-react-ssr") 34 | } 35 | 36 | app.Get("/", func(c *fiber.Ctx) error { 37 | response := engine.RenderRoute(gossr.RenderConfig{ 38 | File: "Home.tsx", 39 | Title: "Fiber example app", 40 | MetaTags: map[string]string{ 41 | "og:title": "Fiber example app", 42 | "description": "Hello world!", 43 | }, 44 | Props: &models.IndexRouteProps{ 45 | InitialCount: rand.Intn(100), 46 | }, 47 | }) 48 | c.Set(fiber.HeaderContentType, fiber.MIMETextHTML) 49 | return c.SendString(string(response)) 50 | }) 51 | 52 | app.Listen(":8080") 53 | } 54 | -------------------------------------------------------------------------------- /examples/fiber/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_main(t *testing.T) { 14 | go main() 15 | // Wait for server to start 16 | for { 17 | conn, _ := net.DialTimeout("tcp", net.JoinHostPort("", "8080"), time.Millisecond*1000) 18 | if conn != nil { 19 | conn.Close() 20 | break 21 | } 22 | } 23 | res, err := http.Get("http://localhost:8080") 24 | assert.Nil(t, err) 25 | assert.Equal(t, 200, res.StatusCode) 26 | 27 | resBody, err := io.ReadAll(res.Body) 28 | assert.Nil(t, err) 29 | assert.NotNil(t, resBody) 30 | assert.NotContains(t, string(resBody), "An error occured!") 31 | } 32 | -------------------------------------------------------------------------------- /examples/fiber/models/props.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type IndexRouteProps struct { 4 | InitialCount int `json:"initialCount"` 5 | } 6 | -------------------------------------------------------------------------------- /examples/frontend-mui/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend-mui", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "@emotion/react": "^11.11.1", 9 | "@emotion/styled": "^11.11.0", 10 | "@mui/material": "^5.14.14", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "^18.2.21", 16 | "@types/react-dom": "^18.2.7", 17 | "typescript": "^5.2.2" 18 | } 19 | }, 20 | "node_modules/@babel/code-frame": { 21 | "version": "7.22.13", 22 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", 23 | "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", 24 | "dependencies": { 25 | "@babel/highlight": "^7.22.13", 26 | "chalk": "^2.4.2" 27 | }, 28 | "engines": { 29 | "node": ">=6.9.0" 30 | } 31 | }, 32 | "node_modules/@babel/helper-module-imports": { 33 | "version": "7.22.15", 34 | "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", 35 | "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", 36 | "dependencies": { 37 | "@babel/types": "^7.22.15" 38 | }, 39 | "engines": { 40 | "node": ">=6.9.0" 41 | } 42 | }, 43 | "node_modules/@babel/helper-string-parser": { 44 | "version": "7.22.5", 45 | "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", 46 | "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", 47 | "engines": { 48 | "node": ">=6.9.0" 49 | } 50 | }, 51 | "node_modules/@babel/helper-validator-identifier": { 52 | "version": "7.22.20", 53 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", 54 | "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", 55 | "engines": { 56 | "node": ">=6.9.0" 57 | } 58 | }, 59 | "node_modules/@babel/highlight": { 60 | "version": "7.22.20", 61 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", 62 | "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", 63 | "dependencies": { 64 | "@babel/helper-validator-identifier": "^7.22.20", 65 | "chalk": "^2.4.2", 66 | "js-tokens": "^4.0.0" 67 | }, 68 | "engines": { 69 | "node": ">=6.9.0" 70 | } 71 | }, 72 | "node_modules/@babel/runtime": { 73 | "version": "7.23.2", 74 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", 75 | "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", 76 | "dependencies": { 77 | "regenerator-runtime": "^0.14.0" 78 | }, 79 | "engines": { 80 | "node": ">=6.9.0" 81 | } 82 | }, 83 | "node_modules/@babel/types": { 84 | "version": "7.23.0", 85 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", 86 | "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", 87 | "dependencies": { 88 | "@babel/helper-string-parser": "^7.22.5", 89 | "@babel/helper-validator-identifier": "^7.22.20", 90 | "to-fast-properties": "^2.0.0" 91 | }, 92 | "engines": { 93 | "node": ">=6.9.0" 94 | } 95 | }, 96 | "node_modules/@emotion/babel-plugin": { 97 | "version": "11.11.0", 98 | "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", 99 | "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", 100 | "dependencies": { 101 | "@babel/helper-module-imports": "^7.16.7", 102 | "@babel/runtime": "^7.18.3", 103 | "@emotion/hash": "^0.9.1", 104 | "@emotion/memoize": "^0.8.1", 105 | "@emotion/serialize": "^1.1.2", 106 | "babel-plugin-macros": "^3.1.0", 107 | "convert-source-map": "^1.5.0", 108 | "escape-string-regexp": "^4.0.0", 109 | "find-root": "^1.1.0", 110 | "source-map": "^0.5.7", 111 | "stylis": "4.2.0" 112 | } 113 | }, 114 | "node_modules/@emotion/cache": { 115 | "version": "11.11.0", 116 | "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", 117 | "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", 118 | "dependencies": { 119 | "@emotion/memoize": "^0.8.1", 120 | "@emotion/sheet": "^1.2.2", 121 | "@emotion/utils": "^1.2.1", 122 | "@emotion/weak-memoize": "^0.3.1", 123 | "stylis": "4.2.0" 124 | } 125 | }, 126 | "node_modules/@emotion/hash": { 127 | "version": "0.9.1", 128 | "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", 129 | "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" 130 | }, 131 | "node_modules/@emotion/is-prop-valid": { 132 | "version": "1.2.1", 133 | "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", 134 | "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", 135 | "dependencies": { 136 | "@emotion/memoize": "^0.8.1" 137 | } 138 | }, 139 | "node_modules/@emotion/memoize": { 140 | "version": "0.8.1", 141 | "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", 142 | "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" 143 | }, 144 | "node_modules/@emotion/react": { 145 | "version": "11.11.1", 146 | "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", 147 | "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", 148 | "dependencies": { 149 | "@babel/runtime": "^7.18.3", 150 | "@emotion/babel-plugin": "^11.11.0", 151 | "@emotion/cache": "^11.11.0", 152 | "@emotion/serialize": "^1.1.2", 153 | "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", 154 | "@emotion/utils": "^1.2.1", 155 | "@emotion/weak-memoize": "^0.3.1", 156 | "hoist-non-react-statics": "^3.3.1" 157 | }, 158 | "peerDependencies": { 159 | "react": ">=16.8.0" 160 | }, 161 | "peerDependenciesMeta": { 162 | "@types/react": { 163 | "optional": true 164 | } 165 | } 166 | }, 167 | "node_modules/@emotion/serialize": { 168 | "version": "1.1.2", 169 | "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", 170 | "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", 171 | "dependencies": { 172 | "@emotion/hash": "^0.9.1", 173 | "@emotion/memoize": "^0.8.1", 174 | "@emotion/unitless": "^0.8.1", 175 | "@emotion/utils": "^1.2.1", 176 | "csstype": "^3.0.2" 177 | } 178 | }, 179 | "node_modules/@emotion/sheet": { 180 | "version": "1.2.2", 181 | "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", 182 | "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" 183 | }, 184 | "node_modules/@emotion/styled": { 185 | "version": "11.11.0", 186 | "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", 187 | "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", 188 | "dependencies": { 189 | "@babel/runtime": "^7.18.3", 190 | "@emotion/babel-plugin": "^11.11.0", 191 | "@emotion/is-prop-valid": "^1.2.1", 192 | "@emotion/serialize": "^1.1.2", 193 | "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", 194 | "@emotion/utils": "^1.2.1" 195 | }, 196 | "peerDependencies": { 197 | "@emotion/react": "^11.0.0-rc.0", 198 | "react": ">=16.8.0" 199 | }, 200 | "peerDependenciesMeta": { 201 | "@types/react": { 202 | "optional": true 203 | } 204 | } 205 | }, 206 | "node_modules/@emotion/unitless": { 207 | "version": "0.8.1", 208 | "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", 209 | "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" 210 | }, 211 | "node_modules/@emotion/use-insertion-effect-with-fallbacks": { 212 | "version": "1.0.1", 213 | "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", 214 | "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", 215 | "peerDependencies": { 216 | "react": ">=16.8.0" 217 | } 218 | }, 219 | "node_modules/@emotion/utils": { 220 | "version": "1.2.1", 221 | "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", 222 | "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" 223 | }, 224 | "node_modules/@emotion/weak-memoize": { 225 | "version": "0.3.1", 226 | "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", 227 | "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" 228 | }, 229 | "node_modules/@floating-ui/core": { 230 | "version": "1.5.0", 231 | "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", 232 | "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", 233 | "dependencies": { 234 | "@floating-ui/utils": "^0.1.3" 235 | } 236 | }, 237 | "node_modules/@floating-ui/dom": { 238 | "version": "1.5.3", 239 | "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", 240 | "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", 241 | "dependencies": { 242 | "@floating-ui/core": "^1.4.2", 243 | "@floating-ui/utils": "^0.1.3" 244 | } 245 | }, 246 | "node_modules/@floating-ui/react-dom": { 247 | "version": "2.0.2", 248 | "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.2.tgz", 249 | "integrity": "sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==", 250 | "dependencies": { 251 | "@floating-ui/dom": "^1.5.1" 252 | }, 253 | "peerDependencies": { 254 | "react": ">=16.8.0", 255 | "react-dom": ">=16.8.0" 256 | } 257 | }, 258 | "node_modules/@floating-ui/utils": { 259 | "version": "0.1.6", 260 | "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", 261 | "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" 262 | }, 263 | "node_modules/@mui/base": { 264 | "version": "5.0.0-beta.20", 265 | "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.20.tgz", 266 | "integrity": "sha512-CS2pUuqxST7ch9VNDCklRYDbJ3rru20Tx7na92QvVVKfu3RL4z/QLuVIc8jYGsdCnauMaeUSlFNLAJNb0yXe6w==", 267 | "dependencies": { 268 | "@babel/runtime": "^7.23.1", 269 | "@floating-ui/react-dom": "^2.0.2", 270 | "@mui/types": "^7.2.6", 271 | "@mui/utils": "^5.14.13", 272 | "@popperjs/core": "^2.11.8", 273 | "clsx": "^2.0.0", 274 | "prop-types": "^15.8.1" 275 | }, 276 | "engines": { 277 | "node": ">=12.0.0" 278 | }, 279 | "funding": { 280 | "type": "opencollective", 281 | "url": "https://opencollective.com/mui" 282 | }, 283 | "peerDependencies": { 284 | "@types/react": "^17.0.0 || ^18.0.0", 285 | "react": "^17.0.0 || ^18.0.0", 286 | "react-dom": "^17.0.0 || ^18.0.0" 287 | }, 288 | "peerDependenciesMeta": { 289 | "@types/react": { 290 | "optional": true 291 | } 292 | } 293 | }, 294 | "node_modules/@mui/core-downloads-tracker": { 295 | "version": "5.14.14", 296 | "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.14.tgz", 297 | "integrity": "sha512-Rw/xKiTOUgXD8hdKqj60aC6QcGprMipG7ne2giK6Mz7b4PlhL/xog9xLeclY3BxsRLkZQ05egFnIEY1CSibTbw==", 298 | "funding": { 299 | "type": "opencollective", 300 | "url": "https://opencollective.com/mui" 301 | } 302 | }, 303 | "node_modules/@mui/material": { 304 | "version": "5.14.14", 305 | "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.14.tgz", 306 | "integrity": "sha512-cAmCwAHFQXxb44kWbVFkhKATN8tACgMsFwrXo8ro6WzYW73U/qsR5AcCiJIhCyYYg+gcftfkmNcpRaV3JjhHCg==", 307 | "dependencies": { 308 | "@babel/runtime": "^7.23.1", 309 | "@mui/base": "5.0.0-beta.20", 310 | "@mui/core-downloads-tracker": "^5.14.14", 311 | "@mui/system": "^5.14.14", 312 | "@mui/types": "^7.2.6", 313 | "@mui/utils": "^5.14.13", 314 | "@types/react-transition-group": "^4.4.7", 315 | "clsx": "^2.0.0", 316 | "csstype": "^3.1.2", 317 | "prop-types": "^15.8.1", 318 | "react-is": "^18.2.0", 319 | "react-transition-group": "^4.4.5" 320 | }, 321 | "engines": { 322 | "node": ">=12.0.0" 323 | }, 324 | "funding": { 325 | "type": "opencollective", 326 | "url": "https://opencollective.com/mui" 327 | }, 328 | "peerDependencies": { 329 | "@emotion/react": "^11.5.0", 330 | "@emotion/styled": "^11.3.0", 331 | "@types/react": "^17.0.0 || ^18.0.0", 332 | "react": "^17.0.0 || ^18.0.0", 333 | "react-dom": "^17.0.0 || ^18.0.0" 334 | }, 335 | "peerDependenciesMeta": { 336 | "@emotion/react": { 337 | "optional": true 338 | }, 339 | "@emotion/styled": { 340 | "optional": true 341 | }, 342 | "@types/react": { 343 | "optional": true 344 | } 345 | } 346 | }, 347 | "node_modules/@mui/private-theming": { 348 | "version": "5.14.14", 349 | "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.14.tgz", 350 | "integrity": "sha512-n77au3CQj9uu16hak2Y+rvbGSBaJKxziG/gEbOLVGrAuqZ+ycVSkorCfN6Y/4XgYOpG/xvmuiY3JwhAEOzY3iA==", 351 | "dependencies": { 352 | "@babel/runtime": "^7.23.1", 353 | "@mui/utils": "^5.14.13", 354 | "prop-types": "^15.8.1" 355 | }, 356 | "engines": { 357 | "node": ">=12.0.0" 358 | }, 359 | "funding": { 360 | "type": "opencollective", 361 | "url": "https://opencollective.com/mui" 362 | }, 363 | "peerDependencies": { 364 | "@types/react": "^17.0.0 || ^18.0.0", 365 | "react": "^17.0.0 || ^18.0.0" 366 | }, 367 | "peerDependenciesMeta": { 368 | "@types/react": { 369 | "optional": true 370 | } 371 | } 372 | }, 373 | "node_modules/@mui/styled-engine": { 374 | "version": "5.14.14", 375 | "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.14.tgz", 376 | "integrity": "sha512-sF3DS2PVG+cFWvkVHQQaGFpL1h6gSwOW3L91pdxPLQDHDZ5mZ/X0SlXU5XA+WjypoysG4urdAQC7CH/BRvUiqg==", 377 | "dependencies": { 378 | "@babel/runtime": "^7.23.1", 379 | "@emotion/cache": "^11.11.0", 380 | "csstype": "^3.1.2", 381 | "prop-types": "^15.8.1" 382 | }, 383 | "engines": { 384 | "node": ">=12.0.0" 385 | }, 386 | "funding": { 387 | "type": "opencollective", 388 | "url": "https://opencollective.com/mui" 389 | }, 390 | "peerDependencies": { 391 | "@emotion/react": "^11.4.1", 392 | "@emotion/styled": "^11.3.0", 393 | "react": "^17.0.0 || ^18.0.0" 394 | }, 395 | "peerDependenciesMeta": { 396 | "@emotion/react": { 397 | "optional": true 398 | }, 399 | "@emotion/styled": { 400 | "optional": true 401 | } 402 | } 403 | }, 404 | "node_modules/@mui/system": { 405 | "version": "5.14.14", 406 | "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.14.tgz", 407 | "integrity": "sha512-y4InFmCgGGWXnz+iK4jRTWVikY0HgYnABjz4wgiUgEa2W1H8M4ow+27BegExUWPkj4TWthQ2qG9FOGSMtI+PKA==", 408 | "dependencies": { 409 | "@babel/runtime": "^7.23.1", 410 | "@mui/private-theming": "^5.14.14", 411 | "@mui/styled-engine": "^5.14.13", 412 | "@mui/types": "^7.2.6", 413 | "@mui/utils": "^5.14.13", 414 | "clsx": "^2.0.0", 415 | "csstype": "^3.1.2", 416 | "prop-types": "^15.8.1" 417 | }, 418 | "engines": { 419 | "node": ">=12.0.0" 420 | }, 421 | "funding": { 422 | "type": "opencollective", 423 | "url": "https://opencollective.com/mui" 424 | }, 425 | "peerDependencies": { 426 | "@emotion/react": "^11.5.0", 427 | "@emotion/styled": "^11.3.0", 428 | "@types/react": "^17.0.0 || ^18.0.0", 429 | "react": "^17.0.0 || ^18.0.0" 430 | }, 431 | "peerDependenciesMeta": { 432 | "@emotion/react": { 433 | "optional": true 434 | }, 435 | "@emotion/styled": { 436 | "optional": true 437 | }, 438 | "@types/react": { 439 | "optional": true 440 | } 441 | } 442 | }, 443 | "node_modules/@mui/types": { 444 | "version": "7.2.6", 445 | "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.6.tgz", 446 | "integrity": "sha512-7sjLQrUmBwufm/M7jw/quNiPK/oor2+pGUQP2CULRcFCArYTq78oJ3D5esTaL0UMkXKJvDqXn6Ike69yAOBQng==", 447 | "peerDependencies": { 448 | "@types/react": "^17.0.0 || ^18.0.0" 449 | }, 450 | "peerDependenciesMeta": { 451 | "@types/react": { 452 | "optional": true 453 | } 454 | } 455 | }, 456 | "node_modules/@mui/utils": { 457 | "version": "5.14.14", 458 | "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.14.tgz", 459 | "integrity": "sha512-3AKp8uksje5sRfVrtgG9Q/2TBsHWVBUtA0NaXliZqGcXo8J+A+Agp0qUW2rJ+ivgPWTCCubz9FZVT2IQZ3bGsw==", 460 | "dependencies": { 461 | "@babel/runtime": "^7.23.1", 462 | "@types/prop-types": "^15.7.7", 463 | "prop-types": "^15.8.1", 464 | "react-is": "^18.2.0" 465 | }, 466 | "engines": { 467 | "node": ">=12.0.0" 468 | }, 469 | "funding": { 470 | "type": "opencollective", 471 | "url": "https://opencollective.com/mui" 472 | }, 473 | "peerDependencies": { 474 | "@types/react": "^17.0.0 || ^18.0.0", 475 | "react": "^17.0.0 || ^18.0.0" 476 | }, 477 | "peerDependenciesMeta": { 478 | "@types/react": { 479 | "optional": true 480 | } 481 | } 482 | }, 483 | "node_modules/@popperjs/core": { 484 | "version": "2.11.8", 485 | "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", 486 | "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", 487 | "funding": { 488 | "type": "opencollective", 489 | "url": "https://opencollective.com/popperjs" 490 | } 491 | }, 492 | "node_modules/@types/parse-json": { 493 | "version": "4.0.1", 494 | "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.1.tgz", 495 | "integrity": "sha512-3YmXzzPAdOTVljVMkTMBdBEvlOLg2cDQaDhnnhT3nT9uDbnJzjWhKlzb+desT12Y7tGqaN6d+AbozcKzyL36Ng==" 496 | }, 497 | "node_modules/@types/prop-types": { 498 | "version": "15.7.9", 499 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", 500 | "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" 501 | }, 502 | "node_modules/@types/react": { 503 | "version": "18.2.21", 504 | "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz", 505 | "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==", 506 | "dependencies": { 507 | "@types/prop-types": "*", 508 | "@types/scheduler": "*", 509 | "csstype": "^3.0.2" 510 | } 511 | }, 512 | "node_modules/@types/react-dom": { 513 | "version": "18.2.7", 514 | "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", 515 | "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", 516 | "dev": true, 517 | "dependencies": { 518 | "@types/react": "*" 519 | } 520 | }, 521 | "node_modules/@types/react-transition-group": { 522 | "version": "4.4.8", 523 | "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.8.tgz", 524 | "integrity": "sha512-QmQ22q+Pb+HQSn04NL3HtrqHwYMf4h3QKArOy5F8U5nEVMaihBs3SR10WiOM1iwPz5jIo8x/u11al+iEGZZrvg==", 525 | "dependencies": { 526 | "@types/react": "*" 527 | } 528 | }, 529 | "node_modules/@types/scheduler": { 530 | "version": "0.16.3", 531 | "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", 532 | "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" 533 | }, 534 | "node_modules/ansi-styles": { 535 | "version": "3.2.1", 536 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 537 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 538 | "dependencies": { 539 | "color-convert": "^1.9.0" 540 | }, 541 | "engines": { 542 | "node": ">=4" 543 | } 544 | }, 545 | "node_modules/babel-plugin-macros": { 546 | "version": "3.1.0", 547 | "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", 548 | "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", 549 | "dependencies": { 550 | "@babel/runtime": "^7.12.5", 551 | "cosmiconfig": "^7.0.0", 552 | "resolve": "^1.19.0" 553 | }, 554 | "engines": { 555 | "node": ">=10", 556 | "npm": ">=6" 557 | } 558 | }, 559 | "node_modules/callsites": { 560 | "version": "3.1.0", 561 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 562 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 563 | "engines": { 564 | "node": ">=6" 565 | } 566 | }, 567 | "node_modules/chalk": { 568 | "version": "2.4.2", 569 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 570 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 571 | "dependencies": { 572 | "ansi-styles": "^3.2.1", 573 | "escape-string-regexp": "^1.0.5", 574 | "supports-color": "^5.3.0" 575 | }, 576 | "engines": { 577 | "node": ">=4" 578 | } 579 | }, 580 | "node_modules/chalk/node_modules/escape-string-regexp": { 581 | "version": "1.0.5", 582 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 583 | "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", 584 | "engines": { 585 | "node": ">=0.8.0" 586 | } 587 | }, 588 | "node_modules/clsx": { 589 | "version": "2.0.0", 590 | "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", 591 | "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", 592 | "engines": { 593 | "node": ">=6" 594 | } 595 | }, 596 | "node_modules/color-convert": { 597 | "version": "1.9.3", 598 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 599 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 600 | "dependencies": { 601 | "color-name": "1.1.3" 602 | } 603 | }, 604 | "node_modules/color-name": { 605 | "version": "1.1.3", 606 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 607 | "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" 608 | }, 609 | "node_modules/convert-source-map": { 610 | "version": "1.9.0", 611 | "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", 612 | "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" 613 | }, 614 | "node_modules/cosmiconfig": { 615 | "version": "7.1.0", 616 | "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", 617 | "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", 618 | "dependencies": { 619 | "@types/parse-json": "^4.0.0", 620 | "import-fresh": "^3.2.1", 621 | "parse-json": "^5.0.0", 622 | "path-type": "^4.0.0", 623 | "yaml": "^1.10.0" 624 | }, 625 | "engines": { 626 | "node": ">=10" 627 | } 628 | }, 629 | "node_modules/csstype": { 630 | "version": "3.1.2", 631 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", 632 | "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" 633 | }, 634 | "node_modules/dom-helpers": { 635 | "version": "5.2.1", 636 | "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", 637 | "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", 638 | "dependencies": { 639 | "@babel/runtime": "^7.8.7", 640 | "csstype": "^3.0.2" 641 | } 642 | }, 643 | "node_modules/error-ex": { 644 | "version": "1.3.2", 645 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", 646 | "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", 647 | "dependencies": { 648 | "is-arrayish": "^0.2.1" 649 | } 650 | }, 651 | "node_modules/escape-string-regexp": { 652 | "version": "4.0.0", 653 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 654 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 655 | "engines": { 656 | "node": ">=10" 657 | }, 658 | "funding": { 659 | "url": "https://github.com/sponsors/sindresorhus" 660 | } 661 | }, 662 | "node_modules/find-root": { 663 | "version": "1.1.0", 664 | "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", 665 | "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" 666 | }, 667 | "node_modules/function-bind": { 668 | "version": "1.1.2", 669 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 670 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 671 | "funding": { 672 | "url": "https://github.com/sponsors/ljharb" 673 | } 674 | }, 675 | "node_modules/has-flag": { 676 | "version": "3.0.0", 677 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 678 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", 679 | "engines": { 680 | "node": ">=4" 681 | } 682 | }, 683 | "node_modules/hasown": { 684 | "version": "2.0.0", 685 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", 686 | "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", 687 | "dependencies": { 688 | "function-bind": "^1.1.2" 689 | }, 690 | "engines": { 691 | "node": ">= 0.4" 692 | } 693 | }, 694 | "node_modules/hoist-non-react-statics": { 695 | "version": "3.3.2", 696 | "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", 697 | "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", 698 | "dependencies": { 699 | "react-is": "^16.7.0" 700 | } 701 | }, 702 | "node_modules/hoist-non-react-statics/node_modules/react-is": { 703 | "version": "16.13.1", 704 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", 705 | "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" 706 | }, 707 | "node_modules/import-fresh": { 708 | "version": "3.3.0", 709 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", 710 | "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", 711 | "dependencies": { 712 | "parent-module": "^1.0.0", 713 | "resolve-from": "^4.0.0" 714 | }, 715 | "engines": { 716 | "node": ">=6" 717 | }, 718 | "funding": { 719 | "url": "https://github.com/sponsors/sindresorhus" 720 | } 721 | }, 722 | "node_modules/is-arrayish": { 723 | "version": "0.2.1", 724 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 725 | "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" 726 | }, 727 | "node_modules/is-core-module": { 728 | "version": "2.13.1", 729 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", 730 | "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", 731 | "dependencies": { 732 | "hasown": "^2.0.0" 733 | }, 734 | "funding": { 735 | "url": "https://github.com/sponsors/ljharb" 736 | } 737 | }, 738 | "node_modules/js-tokens": { 739 | "version": "4.0.0", 740 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 741 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 742 | }, 743 | "node_modules/json-parse-even-better-errors": { 744 | "version": "2.3.1", 745 | "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", 746 | "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" 747 | }, 748 | "node_modules/lines-and-columns": { 749 | "version": "1.2.4", 750 | "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", 751 | "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" 752 | }, 753 | "node_modules/loose-envify": { 754 | "version": "1.4.0", 755 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 756 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 757 | "dependencies": { 758 | "js-tokens": "^3.0.0 || ^4.0.0" 759 | }, 760 | "bin": { 761 | "loose-envify": "cli.js" 762 | } 763 | }, 764 | "node_modules/object-assign": { 765 | "version": "4.1.1", 766 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 767 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 768 | "engines": { 769 | "node": ">=0.10.0" 770 | } 771 | }, 772 | "node_modules/parent-module": { 773 | "version": "1.0.1", 774 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 775 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 776 | "dependencies": { 777 | "callsites": "^3.0.0" 778 | }, 779 | "engines": { 780 | "node": ">=6" 781 | } 782 | }, 783 | "node_modules/parse-json": { 784 | "version": "5.2.0", 785 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", 786 | "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", 787 | "dependencies": { 788 | "@babel/code-frame": "^7.0.0", 789 | "error-ex": "^1.3.1", 790 | "json-parse-even-better-errors": "^2.3.0", 791 | "lines-and-columns": "^1.1.6" 792 | }, 793 | "engines": { 794 | "node": ">=8" 795 | }, 796 | "funding": { 797 | "url": "https://github.com/sponsors/sindresorhus" 798 | } 799 | }, 800 | "node_modules/path-parse": { 801 | "version": "1.0.7", 802 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 803 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" 804 | }, 805 | "node_modules/path-type": { 806 | "version": "4.0.0", 807 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", 808 | "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", 809 | "engines": { 810 | "node": ">=8" 811 | } 812 | }, 813 | "node_modules/prop-types": { 814 | "version": "15.8.1", 815 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", 816 | "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", 817 | "dependencies": { 818 | "loose-envify": "^1.4.0", 819 | "object-assign": "^4.1.1", 820 | "react-is": "^16.13.1" 821 | } 822 | }, 823 | "node_modules/prop-types/node_modules/react-is": { 824 | "version": "16.13.1", 825 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", 826 | "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" 827 | }, 828 | "node_modules/react": { 829 | "version": "18.2.0", 830 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", 831 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", 832 | "dependencies": { 833 | "loose-envify": "^1.1.0" 834 | }, 835 | "engines": { 836 | "node": ">=0.10.0" 837 | } 838 | }, 839 | "node_modules/react-dom": { 840 | "version": "18.2.0", 841 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", 842 | "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", 843 | "dependencies": { 844 | "loose-envify": "^1.1.0", 845 | "scheduler": "^0.23.0" 846 | }, 847 | "peerDependencies": { 848 | "react": "^18.2.0" 849 | } 850 | }, 851 | "node_modules/react-is": { 852 | "version": "18.2.0", 853 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", 854 | "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" 855 | }, 856 | "node_modules/react-transition-group": { 857 | "version": "4.4.5", 858 | "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", 859 | "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", 860 | "dependencies": { 861 | "@babel/runtime": "^7.5.5", 862 | "dom-helpers": "^5.0.1", 863 | "loose-envify": "^1.4.0", 864 | "prop-types": "^15.6.2" 865 | }, 866 | "peerDependencies": { 867 | "react": ">=16.6.0", 868 | "react-dom": ">=16.6.0" 869 | } 870 | }, 871 | "node_modules/regenerator-runtime": { 872 | "version": "0.14.0", 873 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", 874 | "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" 875 | }, 876 | "node_modules/resolve": { 877 | "version": "1.22.8", 878 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", 879 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", 880 | "dependencies": { 881 | "is-core-module": "^2.13.0", 882 | "path-parse": "^1.0.7", 883 | "supports-preserve-symlinks-flag": "^1.0.0" 884 | }, 885 | "bin": { 886 | "resolve": "bin/resolve" 887 | }, 888 | "funding": { 889 | "url": "https://github.com/sponsors/ljharb" 890 | } 891 | }, 892 | "node_modules/resolve-from": { 893 | "version": "4.0.0", 894 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 895 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 896 | "engines": { 897 | "node": ">=4" 898 | } 899 | }, 900 | "node_modules/scheduler": { 901 | "version": "0.23.0", 902 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", 903 | "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", 904 | "dependencies": { 905 | "loose-envify": "^1.1.0" 906 | } 907 | }, 908 | "node_modules/source-map": { 909 | "version": "0.5.7", 910 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 911 | "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", 912 | "engines": { 913 | "node": ">=0.10.0" 914 | } 915 | }, 916 | "node_modules/stylis": { 917 | "version": "4.2.0", 918 | "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", 919 | "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" 920 | }, 921 | "node_modules/supports-color": { 922 | "version": "5.5.0", 923 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 924 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 925 | "dependencies": { 926 | "has-flag": "^3.0.0" 927 | }, 928 | "engines": { 929 | "node": ">=4" 930 | } 931 | }, 932 | "node_modules/supports-preserve-symlinks-flag": { 933 | "version": "1.0.0", 934 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 935 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 936 | "engines": { 937 | "node": ">= 0.4" 938 | }, 939 | "funding": { 940 | "url": "https://github.com/sponsors/ljharb" 941 | } 942 | }, 943 | "node_modules/to-fast-properties": { 944 | "version": "2.0.0", 945 | "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", 946 | "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", 947 | "engines": { 948 | "node": ">=4" 949 | } 950 | }, 951 | "node_modules/typescript": { 952 | "version": "5.2.2", 953 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", 954 | "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", 955 | "dev": true, 956 | "bin": { 957 | "tsc": "bin/tsc", 958 | "tsserver": "bin/tsserver" 959 | }, 960 | "engines": { 961 | "node": ">=14.17" 962 | } 963 | }, 964 | "node_modules/yaml": { 965 | "version": "1.10.2", 966 | "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", 967 | "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", 968 | "engines": { 969 | "node": ">= 6" 970 | } 971 | } 972 | } 973 | } 974 | -------------------------------------------------------------------------------- /examples/frontend-mui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@emotion/react": "^11.11.1", 4 | "@emotion/styled": "^11.11.0", 5 | "@mui/material": "^5.14.14", 6 | "react": "^18.2.0", 7 | "react-dom": "^18.2.0" 8 | }, 9 | "devDependencies": { 10 | "@types/react": "^18.2.21", 11 | "@types/react-dom": "^18.2.7", 12 | "typescript": "^5.2.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/frontend-mui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natewong1313/go-react-ssr/960cd71be1380bf0428d790d111df29e5916e9d4/examples/frontend-mui/public/favicon.ico -------------------------------------------------------------------------------- /examples/frontend-mui/public/go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natewong1313/go-react-ssr/960cd71be1380bf0428d790d111df29e5916e9d4/examples/frontend-mui/public/go.png -------------------------------------------------------------------------------- /examples/frontend-mui/public/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natewong1313/go-react-ssr/960cd71be1380bf0428d790d111df29e5916e9d4/examples/frontend-mui/public/react.png -------------------------------------------------------------------------------- /examples/frontend-mui/src/Home.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #242424; 3 | margin: 0 !important; 4 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 5 | } 6 | 7 | .home { 8 | height: 100vh; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | flex-direction: column; 13 | } 14 | 15 | .img-container { 16 | display: flex; 17 | gap: 24px; 18 | align-items: center; 19 | } 20 | 21 | h1 { 22 | padding-top: 20px; 23 | color: #fff; 24 | font-size: 42px; 25 | margin-bottom: 0; 26 | } 27 | 28 | .counter-container { 29 | padding: 28px 0px; 30 | } 31 | 32 | a { 33 | color: #888; 34 | text-decoration: none; 35 | } 36 | 37 | a:hover { 38 | text-decoration: underline; 39 | } 40 | -------------------------------------------------------------------------------- /examples/frontend-mui/src/Home.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { IndexRouteProps } from "./generated"; 3 | import GoLogo from "../public/go.png"; 4 | import ReactLogo from "../public/react.png"; 5 | import "./Home.css"; 6 | import Counter from "./components/Counter"; 7 | 8 | function Home({ initialCount }: IndexRouteProps) { 9 | const [count, setCount] = useState(initialCount); 10 | 11 | return ( 12 |
13 |
14 | Go logo 15 | React logo 16 |
17 |

Go + React

18 |
19 | setCount(count + 1)} /> 20 |
21 | 22 | View project on GitHub 23 | 24 |
25 | ); 26 | } 27 | 28 | export default Home; 29 | -------------------------------------------------------------------------------- /examples/frontend-mui/src/Layout.tsx: -------------------------------------------------------------------------------- 1 | import createCache from "@emotion/cache"; 2 | import { CacheProvider } from "@emotion/react"; 3 | 4 | const cache = createCache({ key: "css" }) 5 | 6 | export default function Layout({ children }: { children: React.ReactNode }) { 7 | console.log("Hello from Layout"); 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /examples/frontend-mui/src/components/Counter.tsx: -------------------------------------------------------------------------------- 1 | import Button from "@mui/material/Button"; 2 | 3 | type Props = { 4 | count: number; 5 | increment: () => void; 6 | }; 7 | export default function Counter({ count, increment }: Props) { 8 | return ( 9 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/frontend-mui/src/generated.d.ts: -------------------------------------------------------------------------------- 1 | /* Do not change, this code is generated from Golang structs */ 2 | 3 | 4 | export interface IndexRouteProps { 5 | initialCount: number; 6 | } -------------------------------------------------------------------------------- /examples/frontend-mui/src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css"; 2 | declare module "*.png"; 3 | -------------------------------------------------------------------------------- /examples/frontend-mui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | "moduleResolution": "bundler", 8 | "allowImportingTsExtensions": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "noEmit": true, 12 | "jsx": "react-jsx", 13 | "strict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noFallthroughCasesInSwitch": true 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/frontend-tailwind/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "^18.2.0", 4 | "react-dom": "^18.2.0" 5 | }, 6 | "devDependencies": { 7 | "@types/react": "^18.2.21", 8 | "@types/react-dom": "^18.2.7", 9 | "tailwindcss": "^3.3.3", 10 | "typescript": "^5.2.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/frontend-tailwind/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natewong1313/go-react-ssr/960cd71be1380bf0428d790d111df29e5916e9d4/examples/frontend-tailwind/public/favicon.ico -------------------------------------------------------------------------------- /examples/frontend-tailwind/public/go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natewong1313/go-react-ssr/960cd71be1380bf0428d790d111df29e5916e9d4/examples/frontend-tailwind/public/go.png -------------------------------------------------------------------------------- /examples/frontend-tailwind/public/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natewong1313/go-react-ssr/960cd71be1380bf0428d790d111df29e5916e9d4/examples/frontend-tailwind/public/react.png -------------------------------------------------------------------------------- /examples/frontend-tailwind/src/Home.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { IndexRouteProps } from "./generated"; 3 | import GoLogo from "../public/go.png"; 4 | import ReactLogo from "../public/react.png"; 5 | import Counter from "./components/Counter"; 6 | 7 | function Home({ initialCount }: IndexRouteProps) { 8 | const [count, setCount] = useState(initialCount); 9 | 10 | return ( 11 |
12 |
13 | Go logo 14 | React logo 15 |
16 |

Go + React

17 |
18 | setCount(count + 1)} /> 19 |
20 | 21 | View project on GitHub 22 | 23 |
24 | ); 25 | } 26 | 27 | export default Home; 28 | -------------------------------------------------------------------------------- /examples/frontend-tailwind/src/Main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/frontend-tailwind/src/components/Counter.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | count: number; 3 | increment: () => void; 4 | }; 5 | export default function Counter({ count, increment }: Props) { 6 | return ( 7 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /examples/frontend-tailwind/src/generated.d.ts: -------------------------------------------------------------------------------- 1 | /* Do not change, this code is generated from Golang structs */ 2 | 3 | 4 | export interface IndexRouteProps { 5 | initialCount: number; 6 | } -------------------------------------------------------------------------------- /examples/frontend-tailwind/src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css"; 2 | declare module "*.png"; 3 | -------------------------------------------------------------------------------- /examples/frontend-tailwind/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{ts,tsx,js,jsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /examples/frontend-tailwind/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | "moduleResolution": "bundler", 8 | "allowImportingTsExtensions": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "noEmit": true, 12 | "jsx": "react-jsx", 13 | "strict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noFallthroughCasesInSwitch": true 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/frontend/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0" 10 | }, 11 | "devDependencies": { 12 | "@types/react": "^18.2.21", 13 | "@types/react-dom": "^18.2.7", 14 | "typescript": "^5.2.2" 15 | } 16 | }, 17 | "node_modules/@types/prop-types": { 18 | "version": "15.7.5", 19 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", 20 | "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", 21 | "dev": true 22 | }, 23 | "node_modules/@types/react": { 24 | "version": "18.2.21", 25 | "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz", 26 | "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==", 27 | "dev": true, 28 | "dependencies": { 29 | "@types/prop-types": "*", 30 | "@types/scheduler": "*", 31 | "csstype": "^3.0.2" 32 | } 33 | }, 34 | "node_modules/@types/react-dom": { 35 | "version": "18.2.7", 36 | "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", 37 | "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", 38 | "dev": true, 39 | "dependencies": { 40 | "@types/react": "*" 41 | } 42 | }, 43 | "node_modules/@types/scheduler": { 44 | "version": "0.16.3", 45 | "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", 46 | "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", 47 | "dev": true 48 | }, 49 | "node_modules/csstype": { 50 | "version": "3.1.2", 51 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", 52 | "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", 53 | "dev": true 54 | }, 55 | "node_modules/js-tokens": { 56 | "version": "4.0.0", 57 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 58 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 59 | }, 60 | "node_modules/loose-envify": { 61 | "version": "1.4.0", 62 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 63 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 64 | "dependencies": { 65 | "js-tokens": "^3.0.0 || ^4.0.0" 66 | }, 67 | "bin": { 68 | "loose-envify": "cli.js" 69 | } 70 | }, 71 | "node_modules/react": { 72 | "version": "18.2.0", 73 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", 74 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", 75 | "dependencies": { 76 | "loose-envify": "^1.1.0" 77 | }, 78 | "engines": { 79 | "node": ">=0.10.0" 80 | } 81 | }, 82 | "node_modules/react-dom": { 83 | "version": "18.2.0", 84 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", 85 | "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", 86 | "dependencies": { 87 | "loose-envify": "^1.1.0", 88 | "scheduler": "^0.23.0" 89 | }, 90 | "peerDependencies": { 91 | "react": "^18.2.0" 92 | } 93 | }, 94 | "node_modules/scheduler": { 95 | "version": "0.23.0", 96 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", 97 | "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", 98 | "dependencies": { 99 | "loose-envify": "^1.1.0" 100 | } 101 | }, 102 | "node_modules/typescript": { 103 | "version": "5.2.2", 104 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", 105 | "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", 106 | "dev": true, 107 | "bin": { 108 | "tsc": "bin/tsc", 109 | "tsserver": "bin/tsserver" 110 | }, 111 | "engines": { 112 | "node": ">=14.17" 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /examples/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "^18.2.0", 4 | "react-dom": "^18.2.0" 5 | }, 6 | "devDependencies": { 7 | "@types/react": "^18.2.21", 8 | "@types/react-dom": "^18.2.7", 9 | "typescript": "^5.2.2" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natewong1313/go-react-ssr/960cd71be1380bf0428d790d111df29e5916e9d4/examples/frontend/public/favicon.ico -------------------------------------------------------------------------------- /examples/frontend/public/go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natewong1313/go-react-ssr/960cd71be1380bf0428d790d111df29e5916e9d4/examples/frontend/public/go.png -------------------------------------------------------------------------------- /examples/frontend/public/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natewong1313/go-react-ssr/960cd71be1380bf0428d790d111df29e5916e9d4/examples/frontend/public/react.png -------------------------------------------------------------------------------- /examples/frontend/src/Home.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #242424; 3 | margin: 0 !important; 4 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 5 | } 6 | 7 | .home { 8 | height: 100vh; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | flex-direction: column; 13 | } 14 | 15 | .img-container { 16 | display: flex; 17 | gap: 24px; 18 | align-items: center; 19 | } 20 | 21 | h1 { 22 | padding-top: 20px; 23 | color: #fff; 24 | font-size: 42px; 25 | margin-bottom: 0; 26 | } 27 | 28 | .counter-container { 29 | padding: 28px 0px; 30 | } 31 | 32 | a { 33 | color: #888; 34 | text-decoration: none; 35 | } 36 | 37 | a:hover { 38 | text-decoration: underline; 39 | } 40 | -------------------------------------------------------------------------------- /examples/frontend/src/Home.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { IndexRouteProps } from "./generated"; 3 | import GoLogo from "../public/go.png"; 4 | import ReactLogo from "../public/react.png"; 5 | import "./Home.css"; 6 | import Counter from "./components/Counter"; 7 | 8 | function Home({ initialCount }: IndexRouteProps) { 9 | const [count, setCount] = useState(initialCount); 10 | 11 | return ( 12 |
13 |
14 | Go logo 15 | React logo 16 |
17 |

Go + React

18 |
19 | setCount(count + 1)} /> 20 |
21 | 22 | View project on GitHub 23 | 24 |
25 | ); 26 | } 27 | 28 | export default Home; 29 | -------------------------------------------------------------------------------- /examples/frontend/src/Layout.tsx: -------------------------------------------------------------------------------- 1 | export default function Layout({ children }: { children: React.ReactNode }) { 2 | console.log("Hello from Layout"); 3 | return <>{children}; 4 | } 5 | -------------------------------------------------------------------------------- /examples/frontend/src/components/Counter.module.css: -------------------------------------------------------------------------------- 1 | .counter { 2 | color: white; 3 | background-color: #1a1a1a; 4 | font-size: 16px; 5 | padding: 10px 16px; 6 | border: 2px solid transparent; 7 | border-radius: 8px; 8 | cursor: pointer; 9 | } 10 | 11 | .counter:hover { 12 | border: 2px solid #06a8c5; 13 | } 14 | -------------------------------------------------------------------------------- /examples/frontend/src/components/Counter.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Counter.module.css"; 2 | 3 | type Props = { 4 | count: number; 5 | increment: () => void; 6 | }; 7 | export default function Counter({ count, increment }: Props) { 8 | return ( 9 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/frontend/src/generated.d.ts: -------------------------------------------------------------------------------- 1 | /* Do not change, this code is generated from Golang structs */ 2 | 3 | 4 | export interface IndexRouteProps { 5 | initialCount: number; 6 | } -------------------------------------------------------------------------------- /examples/frontend/src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css"; 2 | declare module "*.png"; 3 | -------------------------------------------------------------------------------- /examples/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | "moduleResolution": "bundler", 8 | "allowImportingTsExtensions": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "noEmit": true, 12 | "jsx": "react-jsx", 13 | "strict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noFallthroughCasesInSwitch": true 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/gin/.dockerignore: -------------------------------------------------------------------------------- 1 | */node_modules 2 | .dockerignore 3 | Dockerfile 4 | tmp -------------------------------------------------------------------------------- /examples/gin/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine as build-frontend 2 | ADD ./frontend-tailwind ./build 3 | WORKDIR /build 4 | RUN npm install 5 | 6 | FROM golang:1.21-alpine 7 | RUN apk add git 8 | ADD . /app 9 | COPY --from=build-frontend /build ./app/frontend-tailwind 10 | WORKDIR /app 11 | 12 | RUN go mod download 13 | RUN go get -u github.com/natewong1313/go-react-ssr 14 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-w -X main.APP_ENV=production" -a -o main 15 | 16 | RUN chmod +x ./main 17 | EXPOSE 8080 18 | CMD ["./main"] 19 | -------------------------------------------------------------------------------- /examples/gin/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "1" 2 | services: 3 | app: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | ports: 8 | - "8080:8080" 9 | environment: 10 | - APP_ENV=production 11 | -------------------------------------------------------------------------------- /examples/gin/go.mod: -------------------------------------------------------------------------------- 1 | module example.com/gin 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.9.1 7 | github.com/natewong1313/go-react-ssr v0.0.3 8 | github.com/stretchr/testify v1.8.3 9 | ) 10 | 11 | require ( 12 | github.com/buger/jsonparser v1.1.1 // indirect 13 | github.com/bytedance/sonic v1.9.1 // indirect 14 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 15 | github.com/creasty/defaults v1.7.0 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/evanw/esbuild v0.19.3 // indirect 18 | github.com/fsnotify/fsnotify v1.6.0 // indirect 19 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 20 | github.com/gin-contrib/sse v0.1.0 // indirect 21 | github.com/go-playground/locales v0.14.1 // indirect 22 | github.com/go-playground/universal-translator v0.18.1 // indirect 23 | github.com/go-playground/validator/v10 v10.14.0 // indirect 24 | github.com/goccy/go-json v0.10.2 // indirect 25 | github.com/gorilla/websocket v1.5.0 // indirect 26 | github.com/joho/godotenv v1.5.1 // indirect 27 | github.com/json-iterator/go v1.1.12 // indirect 28 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 29 | github.com/leodido/go-urn v1.2.4 // indirect 30 | github.com/mattn/go-colorable v0.1.13 // indirect 31 | github.com/mattn/go-isatty v0.0.19 // indirect 32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 33 | github.com/modern-go/reflect2 v1.0.2 // indirect 34 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 35 | github.com/pmezard/go-difflib v1.0.0 // indirect 36 | github.com/rs/zerolog v1.30.0 // indirect 37 | github.com/tkrajina/go-reflector v0.5.6 // indirect 38 | github.com/tkrajina/typescriptify-golang-structs v0.1.10 // indirect 39 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 40 | github.com/ugorji/go/codec v1.2.11 // indirect 41 | golang.org/x/arch v0.3.0 // indirect 42 | golang.org/x/crypto v0.9.0 // indirect 43 | golang.org/x/net v0.10.0 // indirect 44 | golang.org/x/sys v0.12.0 // indirect 45 | golang.org/x/text v0.9.0 // indirect 46 | google.golang.org/protobuf v1.30.0 // indirect 47 | gopkg.in/yaml.v3 v3.0.1 // indirect 48 | ) 49 | -------------------------------------------------------------------------------- /examples/gin/go.sum: -------------------------------------------------------------------------------- 1 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 2 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 3 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 4 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= 5 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 6 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 7 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 8 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 9 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 10 | github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= 11 | github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/evanw/esbuild v0.19.3 h1:foPr0xwQM3lBWKBtscauTN9FrmJzRDVI2+EGOs82H/I= 16 | github.com/evanw/esbuild v0.19.3/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk= 17 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 18 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 19 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 20 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 21 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 22 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 23 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 24 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 25 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 26 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 27 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 28 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 29 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 30 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 31 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= 32 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 33 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 34 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 35 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 36 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 37 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 38 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 39 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 40 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 41 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 42 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 43 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 44 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 45 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 46 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 47 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= 48 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 49 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 50 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 51 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 52 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 53 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 54 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 55 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 56 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 57 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 58 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 59 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 60 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 61 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 62 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 63 | github.com/natewong1313/go-react-ssr v0.0.3 h1:fbcwVdAqR7qrtx9Ea8Od54zou1fzn9xEQXBpiqIXmoU= 64 | github.com/natewong1313/go-react-ssr v0.0.3/go.mod h1:mNRYw/XYoE/BEdjDARW+dPqSMr9VE3xRlz7J3BUk1ZE= 65 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= 66 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 67 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 68 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 69 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 70 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 71 | github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= 72 | github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= 73 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 74 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 75 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 76 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 77 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 78 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 79 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 80 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 81 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 82 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 83 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 84 | github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= 85 | github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE= 86 | github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= 87 | github.com/tkrajina/typescriptify-golang-structs v0.1.10 h1:W/Ta9Kqo2lV+7bVXuQoUhZ0bDlnjwtPpKsy3A9M1nYg= 88 | github.com/tkrajina/typescriptify-golang-structs v0.1.10/go.mod h1:sjU00nti/PMEOZb07KljFlR+lJ+RotsC0GBQMv9EKls= 89 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 90 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 91 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 92 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 93 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 94 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= 95 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 96 | golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= 97 | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= 98 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= 99 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 100 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 102 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 103 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 104 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 105 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 106 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 107 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 108 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 109 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 110 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 111 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 112 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 113 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 114 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 115 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 116 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 117 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 118 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 119 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 120 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 121 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 122 | -------------------------------------------------------------------------------- /examples/gin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | 7 | "example.com/gin/models" 8 | 9 | "github.com/gin-gonic/gin" 10 | gossr "github.com/natewong1313/go-react-ssr" 11 | ) 12 | 13 | var APP_ENV string 14 | 15 | func main() { 16 | g := gin.Default() 17 | g.StaticFile("favicon.ico", "../frontend-tailwind/public/favicon.ico") 18 | g.Static("/assets", "../frontend-tailwind/public") 19 | engine, err := gossr.New(gossr.Config{ 20 | AppEnv: APP_ENV, 21 | AssetRoute: "/assets", 22 | FrontendDir: "../frontend-tailwind/src", 23 | GeneratedTypesPath: "../frontend-tailwind/src/generated.d.ts", 24 | TailwindConfigPath: "../frontend-tailwind/tailwind.config.js", 25 | LayoutCSSFilePath: "Main.css", 26 | PropsStructsPath: "./models/props.go", 27 | }) 28 | if err != nil { 29 | log.Fatal("Failed to init go-react-ssr") 30 | } 31 | 32 | g.GET("/", func(c *gin.Context) { 33 | c.Writer.Write(engine.RenderRoute(gossr.RenderConfig{ 34 | File: "Home.tsx", 35 | Title: "Gin example app", 36 | MetaTags: map[string]string{ 37 | "og:title": "Gin example app", 38 | "description": "Hello world!", 39 | }, 40 | Props: &models.IndexRouteProps{ 41 | InitialCount: rand.Intn(100), 42 | }, 43 | })) 44 | }) 45 | g.Run() 46 | } 47 | -------------------------------------------------------------------------------- /examples/gin/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_main(t *testing.T) { 14 | go main() 15 | // Wait for server to start 16 | for { 17 | conn, _ := net.DialTimeout("tcp", net.JoinHostPort("", "8080"), time.Millisecond*1000) 18 | if conn != nil { 19 | conn.Close() 20 | break 21 | } 22 | } 23 | res, err := http.Get("http://localhost:8080") 24 | assert.Nil(t, err) 25 | assert.Equal(t, 200, res.StatusCode) 26 | 27 | resBody, err := io.ReadAll(res.Body) 28 | assert.Nil(t, err) 29 | assert.NotNil(t, resBody) 30 | assert.NotContains(t, string(resBody), "An error occured!") 31 | } 32 | -------------------------------------------------------------------------------- /examples/gin/models/props.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type IndexRouteProps struct { 4 | InitialCount int `json:"initialCount"` 5 | } 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/natewong1313/go-react-ssr 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/buger/jsonparser v1.1.1 7 | github.com/buke/quickjs-go v0.2.4 8 | github.com/evanw/esbuild v0.19.11 9 | github.com/fsnotify/fsnotify v1.7.0 10 | github.com/gorilla/websocket v1.5.1 11 | github.com/rs/zerolog v1.31.0 12 | github.com/tkrajina/typescriptify-golang-structs v0.1.11 13 | ) 14 | 15 | require ( 16 | github.com/mattn/go-colorable v0.1.13 // indirect 17 | github.com/stretchr/testify v1.8.4 // indirect 18 | ) 19 | 20 | require ( 21 | github.com/mattn/go-isatty v0.0.20 // indirect 22 | github.com/tkrajina/go-reflector v0.5.6 // indirect 23 | golang.org/x/net v0.19.0 // indirect 24 | golang.org/x/sys v0.16.0 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 2 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 3 | github.com/buke/quickjs-go v0.2.3 h1:aoQ46FaMUdDhQf57sKKWkDQj1BiAOzJi9v/A7SNVyTU= 4 | github.com/buke/quickjs-go v0.2.3/go.mod h1:d+CLE8FY8v4gQJkPlwcinM+E9mhREMpYy5Zn7nlgE9s= 5 | github.com/buke/quickjs-go v0.2.4 h1:l4Rk6EwgwOcf1JUxFJ+1rVOlGJd1Ofdt+b9U8B6pO1Y= 6 | github.com/buke/quickjs-go v0.2.4/go.mod h1:d+CLE8FY8v4gQJkPlwcinM+E9mhREMpYy5Zn7nlgE9s= 7 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/evanw/esbuild v0.19.3 h1:foPr0xwQM3lBWKBtscauTN9FrmJzRDVI2+EGOs82H/I= 11 | github.com/evanw/esbuild v0.19.3/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk= 12 | github.com/evanw/esbuild v0.19.11 h1:mbPO1VJ/df//jjUd+p/nRLYCpizXxXb2w/zZMShxa2k= 13 | github.com/evanw/esbuild v0.19.11/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= 14 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 15 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 16 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 17 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 18 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 19 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 20 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 21 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 22 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 23 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 24 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 25 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 26 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 27 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 28 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 29 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 30 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 31 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 32 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 33 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 34 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 35 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 36 | github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= 37 | github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= 38 | github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= 39 | github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 40 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 41 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 42 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 43 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 44 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 45 | github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ= 46 | github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= 47 | github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE= 48 | github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= 49 | github.com/tkrajina/typescriptify-golang-structs v0.1.10 h1:W/Ta9Kqo2lV+7bVXuQoUhZ0bDlnjwtPpKsy3A9M1nYg= 50 | github.com/tkrajina/typescriptify-golang-structs v0.1.10/go.mod h1:sjU00nti/PMEOZb07KljFlR+lJ+RotsC0GBQMv9EKls= 51 | github.com/tkrajina/typescriptify-golang-structs v0.1.11 h1:zEIVczF/iWgs4eTY7NQqbBe23OVlFVk9sWLX/FDYi4Q= 52 | github.com/tkrajina/typescriptify-golang-structs v0.1.11/go.mod h1:sjU00nti/PMEOZb07KljFlR+lJ+RotsC0GBQMv9EKls= 53 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= 54 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 55 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 57 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 62 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 64 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 65 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 66 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 67 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 68 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.20 2 | 3 | use ( 4 | . 5 | ./examples/fiber 6 | ./examples/gin 7 | ./examples/echo 8 | ./gossr-cli 9 | ) 10 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 3 | github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= 4 | github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d h1:W1n4DvpzZGOISgp7wWNtraLcHtnmnTwBlJidqtMIuwQ= 5 | github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= 6 | github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= 7 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 8 | github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= 9 | github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2 h1:rcanfLhLDA8nozr/K289V1zcntHr3V+SHlXwzz1ZI2g= 10 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 11 | github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= 12 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 13 | github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= 14 | github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= 15 | github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= 16 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 17 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 18 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 19 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 20 | github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= 21 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 22 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 23 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 24 | github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= 25 | github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= 26 | github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= 27 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 28 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 29 | go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 30 | go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 31 | golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= 32 | golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 33 | golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= 34 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 35 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 36 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 37 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 38 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 42 | golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= 43 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 44 | golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= 45 | golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= 46 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 47 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 48 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 49 | golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= 50 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 51 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 52 | gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= 53 | rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= 54 | -------------------------------------------------------------------------------- /gossr-cli/cmd/create/bootstrapper.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strings" 7 | "sync" 8 | 9 | cp "github.com/otiai10/copy" 10 | 11 | "github.com/natewong1313/go-react-ssr/gossr-cli/logger" 12 | "github.com/natewong1313/go-react-ssr/gossr-cli/utils" 13 | ) 14 | 15 | type Bootstrapper struct { 16 | TempDirPath string 17 | ProjectDir string 18 | GoModuleName string 19 | FrontendDir string 20 | WebFramework string 21 | StylingPlugin string 22 | } 23 | 24 | func (b *Bootstrapper) Start() { 25 | b.TempDirPath = createTempDir() 26 | b.cloneRepo() 27 | b.moveGoFiles() 28 | var wg sync.WaitGroup 29 | wg.Add(2) 30 | go b.setupFrontend(&wg) 31 | go b.setupBackend(&wg) 32 | wg.Wait() 33 | logger.L.Info().Msg("Project setup complete! 🎉") 34 | } 35 | 36 | func (b *Bootstrapper) cloneRepo() { 37 | logger.L.Info().Msg("Cloning example repository") 38 | cmd := exec.Command("git", "clone", "https://github.com/natewong1313/go-react-ssr.git") 39 | cmd.Dir = b.TempDirPath 40 | err := cmd.Run() 41 | if err != nil { 42 | utils.HandleError(err) 43 | } 44 | } 45 | 46 | func (b *Bootstrapper) moveGoFiles() { 47 | logger.L.Info().Msg("Setting up Go files") 48 | err := cp.Copy(b.TempDirPath+"/go-react-ssr/examples/"+strings.ToLower(b.WebFramework), b.ProjectDir) 49 | if err != nil { 50 | utils.HandleError(err) 51 | } 52 | } 53 | 54 | func (b *Bootstrapper) setupFrontend(wg *sync.WaitGroup) { 55 | b.createFrontendFolder() 56 | b.installNPMDependencies() 57 | wg.Done() 58 | } 59 | 60 | func (b *Bootstrapper) setupBackend(wg *sync.WaitGroup) { 61 | b.updateGoModules() 62 | b.replaceImportsInGoFile() 63 | b.updateDockerFile() 64 | wg.Done() 65 | } 66 | 67 | func (b *Bootstrapper) createFrontendFolder() { 68 | logger.L.Info().Msg("Creating /frontend folder") 69 | frontendFolderFromGit := b.TempDirPath + "/go-react-ssr/examples/frontend" 70 | if b.StylingPlugin == "Tailwind" { 71 | frontendFolderFromGit = b.TempDirPath + "/go-react-ssr/examples/frontend-tailwind" 72 | } else if b.StylingPlugin == "Material UI" { 73 | frontendFolderFromGit = b.TempDirPath + "/go-react-ssr/examples/frontend-mui" 74 | } 75 | err := cp.Copy(frontendFolderFromGit, b.ProjectDir+"/frontend") 76 | if err != nil { 77 | utils.HandleError(err) 78 | } 79 | } 80 | 81 | func (b *Bootstrapper) installNPMDependencies() { 82 | logger.L.Info().Msg("Installing npm dependencies") 83 | cmd := exec.Command("npm", "install") 84 | cmd.Dir = b.ProjectDir + "/frontend" 85 | err := cmd.Run() 86 | if err != nil { 87 | utils.HandleError(err) 88 | } 89 | } 90 | 91 | func (b *Bootstrapper) updateGoModules() { 92 | logger.L.Info().Msg("Installing Go modules") 93 | cmd := exec.Command("go", "get", "-u", "github.com/natewong1313/go-react-ssr") 94 | cmd.Dir = b.ProjectDir 95 | err := cmd.Run() 96 | if err != nil { 97 | utils.HandleError(err) 98 | } 99 | } 100 | 101 | func (b *Bootstrapper) replaceImportsInGoFile() { 102 | logger.L.Info().Msg("Updating imports in main.go") 103 | read, err := os.ReadFile(b.ProjectDir + "/main.go") 104 | if err != nil { 105 | utils.HandleError(err) 106 | } 107 | newContents := strings.Replace(string(read), "../frontend", "./frontend", -1) 108 | newContents = strings.Replace(newContents, "-tailwind/", "/", -1) 109 | newContents = strings.Replace(newContents, "-mui/", "/", -1) 110 | err = os.WriteFile(b.ProjectDir+"/main.go", []byte(newContents), 0644) 111 | if err != nil { 112 | utils.HandleError(err) 113 | } 114 | } 115 | 116 | func (b *Bootstrapper) updateDockerFile() { 117 | logger.L.Info().Msg("Updating Dockerfile") 118 | read, err := os.ReadFile(b.ProjectDir + "/Dockerfile") 119 | if err != nil { 120 | utils.HandleError(err) 121 | } 122 | var contents string 123 | contents = strings.Replace(string(read), "frontend-tailwind", "frontend", -1) 124 | contents = strings.Replace(string(read), "frontend-mui", "frontend", -1) 125 | err = os.WriteFile(b.ProjectDir+"/Dockerfile", []byte(contents), 0644) 126 | if err != nil { 127 | utils.HandleError(err) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /gossr-cli/cmd/create/create.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | 8 | "github.com/natewong1313/go-react-ssr/gossr-cli/cmd" 9 | "github.com/natewong1313/go-react-ssr/gossr-cli/logger" 10 | "github.com/natewong1313/go-react-ssr/gossr-cli/utils" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // createCmd represents the create command 15 | var createCmd = &cobra.Command{ 16 | Use: "create", 17 | Short: "Create a new Go SSR project", 18 | Long: "Create a new Go SSR project", 19 | Run: create, 20 | } 21 | 22 | func init() { 23 | cmd.RootCmd.AddCommand(createCmd) 24 | } 25 | 26 | func create(cmd *cobra.Command, args []string) { 27 | checkNodeInstalled() 28 | fmt.Println("Welcome to the creation wizard!") 29 | projectDir := prompt_getProjectDirectory(args) 30 | webFramework := prompt_selectWebFramework() 31 | stylingPlugin := prompt_selectStylingPlugin() 32 | 33 | projectDirExists := utils.CheckPathExists(projectDir) 34 | if projectDirExists { 35 | projectDirEmpty := utils.CheckPathEmpty(projectDir) 36 | if !projectDirEmpty && !prompt_shouldWipeDirectory() { 37 | os.Exit(0) 38 | } else { 39 | wipeDirectory(projectDir) 40 | } 41 | 42 | } else { 43 | if err := os.MkdirAll(projectDir, 0777); err != nil { 44 | utils.HandleError(err) 45 | } 46 | } 47 | 48 | bootstrapper := Bootstrapper{ 49 | ProjectDir: projectDir, 50 | WebFramework: webFramework, 51 | StylingPlugin: stylingPlugin, 52 | } 53 | bootstrapper.Start() 54 | 55 | } 56 | 57 | func checkNodeInstalled() bool { 58 | cmd := exec.Command("node", "-v") 59 | err := cmd.Run() 60 | if err != nil { 61 | logger.L.Error().Msg("Node.js is not installed. Please install Node and try again.") 62 | os.Exit(1) 63 | } 64 | return true 65 | } 66 | 67 | func wipeDirectory(projectDir string) { 68 | logger.L.Info().Msg("Wiping directory " + projectDir) 69 | if err := os.RemoveAll(projectDir); err != nil { 70 | utils.HandleError(err) 71 | } 72 | if err := os.MkdirAll(projectDir, 0777); err != nil { 73 | utils.HandleError(err) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /gossr-cli/cmd/create/prompts.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "github.com/manifoldco/promptui" 8 | "github.com/natewong1313/go-react-ssr/gossr-cli/logger" 9 | "github.com/natewong1313/go-react-ssr/gossr-cli/utils" 10 | ) 11 | 12 | func prompt_getProjectDirectory(args []string) string { 13 | display := func(path string) string { 14 | logger.L.Info().Msg("Creating project at " + path) 15 | return path 16 | } 17 | if len(args) > 0 { 18 | projectDir, err := filepath.Abs(args[0]) 19 | projectDir = filepath.ToSlash(projectDir) 20 | if err != nil { 21 | utils.HandleError(err) 22 | } 23 | return display(projectDir) 24 | } 25 | 26 | prompt := promptui.Prompt{ 27 | Label: "Enter the path of your project (leave blank to use current directory)", 28 | } 29 | 30 | result, err := prompt.Run() 31 | projectDir, err := filepath.Abs(result) 32 | if err != nil { 33 | utils.HandleError(err) 34 | } 35 | projectDir = filepath.ToSlash(projectDir) 36 | return display(projectDir) 37 | } 38 | 39 | func prompt_selectWebFramework() string { 40 | prompt := promptui.Select{ 41 | Label: "Select a web framework to use", 42 | Items: []string{"Gin", "Fiber", "Echo"}, 43 | } 44 | 45 | _, result, err := prompt.Run() 46 | if err != nil { 47 | utils.HandleError(err) 48 | } 49 | return result 50 | } 51 | 52 | func prompt_selectStylingPlugin() string { 53 | prompt := promptui.Select{ 54 | Label: "Select a styling plugin to use", 55 | Items: []string{"None", "Tailwind", "Material UI"}, 56 | } 57 | 58 | _, result, err := prompt.Run() 59 | if err != nil { 60 | utils.HandleError(err) 61 | } 62 | return result 63 | } 64 | 65 | func prompt_isUsingTypescript() bool { 66 | prompt := promptui.Prompt{ 67 | Label: "Use Typescript? (y/n)", 68 | Default: "y", 69 | } 70 | 71 | result, err := prompt.Run() 72 | if err != nil { 73 | utils.HandleError(err) 74 | } 75 | return strings.ToLower(result) == "y" 76 | } 77 | 78 | func prompt_shouldWipeDirectory() bool { 79 | prompt := promptui.Prompt{ 80 | Label: "Directory is not empty. Continue? (this will wipe the directory) (y/n)", 81 | Default: "n", 82 | } 83 | 84 | result, err := prompt.Run() 85 | if err != nil { 86 | utils.HandleError(err) 87 | } 88 | return strings.ToLower(result) == "y" 89 | } 90 | -------------------------------------------------------------------------------- /gossr-cli/cmd/create/temp_dir.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | func createTempDir() string { 9 | osCacheDir, _ := os.UserCacheDir() 10 | tempDirPath := filepath.Join(osCacheDir, "gossr-cli") 11 | os.RemoveAll(tempDirPath) 12 | os.MkdirAll(tempDirPath, os.ModePerm) 13 | 14 | return tempDirPath 15 | } 16 | -------------------------------------------------------------------------------- /gossr-cli/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/natewong1313/go-react-ssr/gossr-cli/logger" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // rootCmd represents the base command when called without any subcommands 11 | var RootCmd = &cobra.Command{ 12 | Use: "gossr-cli", 13 | Short: "This application helps you get a go-react-ssr powered app up and running in no time.", 14 | Long: `This application helps you get a go-react-ssr powered app up and running in no time. 15 | Complete documentation is available at https://github.com/natewong1313/go-react-ssr`, 16 | } 17 | 18 | // Execute adds all child commands to the root command and sets flags appropriately. 19 | // This is called by main.main(). It only needs to happen once to the rootCmd. 20 | func Execute() { 21 | logger.Init() 22 | err := RootCmd.Execute() 23 | if err != nil { 24 | os.Exit(1) 25 | } 26 | } 27 | 28 | func init() { 29 | // Here you will define your flags and configuration settings. 30 | // Cobra supports persistent flags, which, if defined here, 31 | // will be global for your application. 32 | 33 | // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.test.yaml)") 34 | 35 | // Cobra also supports local flags, which will only run 36 | // when this action is called directly. 37 | } 38 | -------------------------------------------------------------------------------- /gossr-cli/cmd/update/check_update.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/natewong1313/go-react-ssr/gossr-cli/utils" 9 | ) 10 | 11 | func CheckNeedsUpdate() bool { 12 | configDirPath := getConfigDir() 13 | VersionFilePath = filepath.Join(configDirPath, "version") 14 | 15 | if _, err := os.Stat(VersionFilePath); errors.Is(err, os.ErrNotExist) { 16 | createVersionFile() 17 | return false 18 | } 19 | currentVersion, err := os.ReadFile(VersionFilePath) 20 | if err != nil { 21 | utils.HandleError(err) 22 | } 23 | return string(currentVersion) != getLatestVersion() 24 | } 25 | -------------------------------------------------------------------------------- /gossr-cli/cmd/update/latest_version.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | 7 | "github.com/buger/jsonparser" 8 | "github.com/natewong1313/go-react-ssr/gossr-cli/utils" 9 | ) 10 | 11 | func getLatestVersion() string { 12 | res, err := http.Get("https://proxy.golang.org/github.com/natewong1313/go-react-ssr/@latest") 13 | if err != nil { 14 | utils.HandleError(err) 15 | } 16 | defer res.Body.Close() 17 | resBody, err := io.ReadAll(res.Body) 18 | if err != nil { 19 | utils.HandleError(err) 20 | } 21 | version, err := jsonparser.GetString(resBody, "Version") 22 | if err != nil { 23 | utils.HandleError(err) 24 | } 25 | return version 26 | } 27 | -------------------------------------------------------------------------------- /gossr-cli/cmd/update/update.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "os/exec" 5 | 6 | "github.com/natewong1313/go-react-ssr/gossr-cli/cmd" 7 | "github.com/natewong1313/go-react-ssr/gossr-cli/logger" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // updateCmd represents the update command 12 | var updateCmd = &cobra.Command{ 13 | Use: "update", 14 | Short: "Update the cli to the latest version", 15 | Long: "Update the cli to the latest version", 16 | Run: update, 17 | } 18 | 19 | func init() { 20 | if CheckNeedsUpdate() { 21 | cmd.RootCmd.AddCommand(updateCmd) 22 | } 23 | } 24 | 25 | func update(cmd *cobra.Command, args []string) { 26 | exec.Command("go", "install", "github.com/natewong1313/go-react-ssr/gossr-cli@latest").Run() 27 | updateVersionFile() 28 | logger.L.Info().Msg("Updated to latest version!") 29 | } 30 | -------------------------------------------------------------------------------- /gossr-cli/cmd/update/version_file.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/natewong1313/go-react-ssr/gossr-cli/utils" 8 | ) 9 | 10 | var VersionFilePath string 11 | 12 | func getConfigDir() string { 13 | configDir, _ := os.UserConfigDir() 14 | configDirPath := filepath.Join(configDir, "gossr-cli") 15 | os.MkdirAll(configDirPath, os.ModePerm) 16 | return configDirPath 17 | } 18 | 19 | func createVersionFile() { 20 | file, err := os.Create(VersionFilePath) 21 | if err != nil { 22 | utils.HandleError(err) 23 | } 24 | defer file.Close() 25 | file.WriteString(getLatestVersion()) 26 | } 27 | 28 | func updateVersionFile() { 29 | file, err := os.OpenFile(VersionFilePath, os.O_WRONLY, 0644) 30 | if err != nil { 31 | utils.HandleError(err) 32 | } 33 | defer file.Close() 34 | file.WriteString(getLatestVersion()) 35 | } 36 | -------------------------------------------------------------------------------- /gossr-cli/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/natewong1313/go-react-ssr/gossr-cli 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/buger/jsonparser v1.1.1 7 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be 8 | github.com/fatih/color v1.15.0 9 | github.com/manifoldco/promptui v0.9.0 10 | github.com/otiai10/copy v1.12.0 11 | github.com/rs/zerolog v1.31.0 12 | github.com/spf13/cobra v1.7.0 13 | ) 14 | 15 | require ( 16 | github.com/chzyer/readline v1.5.1 // indirect 17 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 18 | github.com/mattn/go-colorable v0.1.13 // indirect 19 | github.com/mattn/go-isatty v0.0.19 // indirect 20 | github.com/spf13/pflag v1.0.5 // indirect 21 | golang.org/x/sys v0.12.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /gossr-cli/go.sum: -------------------------------------------------------------------------------- 1 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 2 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 3 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 4 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 5 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 6 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 7 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 8 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 9 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 10 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 11 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 12 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= 13 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= 14 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 15 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 16 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 17 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 18 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 19 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 20 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 21 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 22 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 23 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 24 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 25 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 26 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 27 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 28 | github.com/otiai10/copy v1.12.0 h1:cLMgSQnXBs1eehF0Wy/FAGsgDTDmAqFR7rQylBb1nDY= 29 | github.com/otiai10/copy v1.12.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww= 30 | github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= 31 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 32 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 33 | github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= 34 | github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 35 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 36 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 37 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 38 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 39 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 40 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 41 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 45 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 48 | -------------------------------------------------------------------------------- /gossr-cli/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/rs/zerolog" 7 | ) 8 | 9 | var L zerolog.Logger 10 | 11 | // Init initializes a global logger instance 12 | func Init() { 13 | L = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger() 14 | } 15 | -------------------------------------------------------------------------------- /gossr-cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/common-nighthawk/go-figure" 7 | "github.com/fatih/color" 8 | "github.com/natewong1313/go-react-ssr/gossr-cli/cmd" 9 | _ "github.com/natewong1313/go-react-ssr/gossr-cli/cmd/create" 10 | "github.com/natewong1313/go-react-ssr/gossr-cli/cmd/update" 11 | ) 12 | 13 | func main() { 14 | art := figure.NewFigure("Go - SSR CLI", "slant", true) 15 | art.Print() 16 | fmt.Println() 17 | if update.CheckNeedsUpdate() { 18 | color.Magenta("🚨 A new version of gossr-cli is available! Run `gossr-cli update` to update. 🚨\n\n") 19 | } 20 | cmd.Execute() 21 | } 22 | -------------------------------------------------------------------------------- /gossr-cli/utils/error.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | 7 | "github.com/natewong1313/go-react-ssr/gossr-cli/logger" 8 | ) 9 | 10 | func HandleError(err error) { 11 | if err.Error() == "^C" { 12 | logger.L.Info().Msg("Goodbye 👋") 13 | } else { 14 | _, filename, line, _ := runtime.Caller(1) 15 | logger.L.Error().Err(err).Msgf("An error occurred in [%s:%d]", filename, line) 16 | } 17 | os.Exit(1) 18 | } 19 | -------------------------------------------------------------------------------- /gossr-cli/utils/path.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | func CheckPathExists(projectDir string) bool { 9 | if _, err := os.Stat(projectDir); os.IsNotExist(err) { 10 | return false 11 | } 12 | return true 13 | } 14 | 15 | func CheckPathEmpty(projectDir string) bool { 16 | f, err := os.Open(projectDir) 17 | if err != nil { 18 | HandleError(err) 19 | } 20 | defer f.Close() 21 | 22 | if _, err = f.Readdirnames(1); err == io.EOF { 23 | return true 24 | } 25 | return false 26 | } 27 | -------------------------------------------------------------------------------- /hotreload.go: -------------------------------------------------------------------------------- 1 | package go_ssr 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fsnotify/fsnotify" 6 | "github.com/gorilla/websocket" 7 | "github.com/natewong1313/go-react-ssr/internal/utils" 8 | "github.com/rs/zerolog" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | type HotReload struct { 16 | engine *Engine 17 | logger zerolog.Logger 18 | connectedClients map[string][]*websocket.Conn 19 | } 20 | 21 | // newHotReload creates a new HotReload instance 22 | func newHotReload(engine *Engine) *HotReload { 23 | return &HotReload{ 24 | engine: engine, 25 | logger: engine.Logger, 26 | connectedClients: make(map[string][]*websocket.Conn), 27 | } 28 | } 29 | 30 | // Start starts the hot reload server and watcher 31 | func (hr *HotReload) Start() { 32 | go hr.startServer() 33 | go hr.startWatcher() 34 | } 35 | 36 | // startServer starts the hot reload websocket server 37 | func (hr *HotReload) startServer() { 38 | hr.logger.Info().Msgf("Hot reload websocket running on port %d", hr.engine.Config.HotReloadServerPort) 39 | upgrader := websocket.Upgrader{ 40 | CheckOrigin: func(r *http.Request) bool { 41 | return true 42 | }, 43 | } 44 | http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { 45 | ws, err := upgrader.Upgrade(w, r, nil) 46 | if err != nil { 47 | hr.logger.Err(err).Msg("Failed to upgrade websocket") 48 | return 49 | } 50 | // Client should send routeID as first message 51 | _, routeID, err := ws.ReadMessage() 52 | if err != nil { 53 | hr.logger.Err(err).Msg("Failed to read message from websocket") 54 | return 55 | } 56 | err = ws.WriteMessage(1, []byte("Connected")) 57 | if err != nil { 58 | hr.logger.Err(err).Msg("Failed to write message to websocket") 59 | return 60 | } 61 | // Add client to connectedClients 62 | hr.connectedClients[string(routeID)] = append(hr.connectedClients[string(routeID)], ws) 63 | }) 64 | err := http.ListenAndServe(fmt.Sprintf(":%d", hr.engine.Config.HotReloadServerPort), nil) 65 | if err != nil { 66 | hr.logger.Err(err).Msg("Hot reload server quit unexpectedly") 67 | } 68 | } 69 | 70 | // startWatcher starts the file watcher 71 | func (hr *HotReload) startWatcher() { 72 | watcher, err := fsnotify.NewWatcher() 73 | if err != nil { 74 | hr.logger.Err(err).Msg("Failed to start watcher") 75 | return 76 | } 77 | defer watcher.Close() 78 | // Walk through all files in the frontend directory and add them to the watcher 79 | if err = filepath.Walk(hr.engine.Config.FrontendDir, func(path string, fi os.FileInfo, err error) error { 80 | if fi.Mode().IsDir() { 81 | return watcher.Add(path) 82 | } 83 | return nil 84 | }); err != nil { 85 | hr.logger.Err(err).Msg("Failed to add files in directory to watcher") 86 | return 87 | } 88 | 89 | for { 90 | select { 91 | case event := <-watcher.Events: 92 | // Watch for file created, deleted, updated, or renamed events 93 | if event.Op.String() != "CHMOD" && !strings.Contains(event.Name, "gossr-temporary") { 94 | filePath := utils.GetFullFilePath(event.Name) 95 | hr.logger.Info().Msgf("File changed: %s, reloading", filePath) 96 | // Store the routes that need to be reloaded 97 | var routeIDS []string 98 | switch { 99 | case filePath == hr.engine.Config.LayoutFilePath: // If the layout file has been updated, reload all routes 100 | routeIDS = hr.engine.CacheManager.GetAllRouteIDS() 101 | case hr.layoutCSSFileUpdated(filePath): // If the global css file has been updated, rebuild it and reload all routes 102 | if err := hr.engine.BuildLayoutCSSFile(); err != nil { 103 | hr.logger.Err(err).Msg("Failed to build global css file") 104 | continue 105 | } 106 | routeIDS = hr.engine.CacheManager.GetAllRouteIDS() 107 | case hr.needsTailwindRecompile(filePath): // If tailwind is enabled and a React file has been updated, rebuild the global css file and reload all routes 108 | if err := hr.engine.BuildLayoutCSSFile(); err != nil { 109 | hr.logger.Err(err).Msg("Failed to build global css file") 110 | continue 111 | } 112 | fallthrough 113 | default: 114 | // Get all route ids that use that file or have it as a dependency 115 | routeIDS = hr.engine.CacheManager.GetRouteIDSWithFile(filePath) 116 | } 117 | // Find any parent files that import the file that was modified and delete their cached build 118 | parentFiles := hr.engine.CacheManager.GetParentFilesFromDependency(filePath) 119 | for _, parentFile := range parentFiles { 120 | hr.engine.CacheManager.RemoveServerBuild(parentFile) 121 | hr.engine.CacheManager.RemoveClientBuild(parentFile) 122 | } 123 | // Reload any routes that import the modified file 124 | go hr.broadcastFileUpdateToClients(routeIDS) 125 | 126 | } 127 | case err := <-watcher.Errors: 128 | hr.logger.Err(err).Msg("Error watching files") 129 | } 130 | } 131 | } 132 | 133 | // layoutCSSFileUpdated checks if the layout css file has been updated 134 | func (hr *HotReload) layoutCSSFileUpdated(filePath string) bool { 135 | return utils.GetFullFilePath(filePath) == hr.engine.Config.LayoutCSSFilePath 136 | } 137 | 138 | // needsTailwindRecompile checks if the file that was updated is a React file 139 | func (hr *HotReload) needsTailwindRecompile(filePath string) bool { 140 | if hr.engine.Config.TailwindConfigPath == "" { 141 | return false 142 | } 143 | fileTypes := []string{".tsx", ".ts", ".jsx", ".js"} 144 | for _, fileType := range fileTypes { 145 | if strings.HasSuffix(filePath, fileType) { 146 | return true 147 | } 148 | } 149 | return false 150 | } 151 | 152 | // broadcastFileUpdateToClients sends a message to all connected clients to reload the page 153 | func (hr *HotReload) broadcastFileUpdateToClients(routeIDS []string) { 154 | // Iterate over each route ID 155 | for _, routeID := range routeIDS { 156 | // Find all clients listening for that route ID 157 | for i, ws := range hr.connectedClients[routeID] { 158 | // Send reload message to client 159 | err := ws.WriteMessage(1, []byte("reload")) 160 | if err != nil { 161 | // remove client if browser is closed or page changed 162 | hr.connectedClients[routeID] = append(hr.connectedClients[routeID][:i], hr.connectedClients[routeID][i+1:]...) 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /internal/cache/manager.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/natewong1313/go-react-ssr/internal/reactbuilder" 5 | "sync" 6 | ) 7 | 8 | type Manager struct { 9 | serverBuilds *serverBuilds 10 | clientBuilds *clientBuilds 11 | routeIDToParentFile *routeIDToParentFile 12 | parentFileToDependencies *parentFileToDependencies 13 | } 14 | 15 | func NewManager() *Manager { 16 | return &Manager{ 17 | serverBuilds: &serverBuilds{ 18 | builds: make(map[string]reactbuilder.BuildResult), 19 | lock: sync.RWMutex{}, 20 | }, 21 | clientBuilds: &clientBuilds{ 22 | builds: make(map[string]reactbuilder.BuildResult), 23 | lock: sync.RWMutex{}, 24 | }, 25 | routeIDToParentFile: &routeIDToParentFile{ 26 | reactFiles: make(map[string]string), 27 | lock: sync.RWMutex{}, 28 | }, 29 | parentFileToDependencies: &parentFileToDependencies{ 30 | dependencies: make(map[string][]string), 31 | lock: sync.RWMutex{}, 32 | }, 33 | } 34 | } 35 | 36 | type serverBuilds struct { 37 | builds map[string]reactbuilder.BuildResult 38 | lock sync.RWMutex 39 | } 40 | 41 | func (cm *Manager) GetServerBuild(filePath string) (reactbuilder.BuildResult, bool) { 42 | cm.serverBuilds.lock.RLock() 43 | defer cm.serverBuilds.lock.RUnlock() 44 | build, ok := cm.serverBuilds.builds[filePath] 45 | return build, ok 46 | } 47 | 48 | func (cm *Manager) SetServerBuild(filePath string, build reactbuilder.BuildResult) { 49 | cm.serverBuilds.lock.Lock() 50 | defer cm.serverBuilds.lock.Unlock() 51 | cm.serverBuilds.builds[filePath] = build 52 | } 53 | 54 | func (cm *Manager) RemoveServerBuild(filePath string) { 55 | cm.serverBuilds.lock.Lock() 56 | defer cm.serverBuilds.lock.Unlock() 57 | if _, ok := cm.serverBuilds.builds[filePath]; !ok { 58 | return 59 | } 60 | delete(cm.serverBuilds.builds, filePath) 61 | } 62 | 63 | type clientBuilds struct { 64 | builds map[string]reactbuilder.BuildResult 65 | lock sync.RWMutex 66 | } 67 | 68 | func (cm *Manager) GetClientBuild(filePath string) (reactbuilder.BuildResult, bool) { 69 | cm.clientBuilds.lock.RLock() 70 | defer cm.clientBuilds.lock.RUnlock() 71 | build, ok := cm.clientBuilds.builds[filePath] 72 | return build, ok 73 | } 74 | 75 | func (cm *Manager) SetClientBuild(filePath string, build reactbuilder.BuildResult) { 76 | cm.clientBuilds.lock.Lock() 77 | defer cm.clientBuilds.lock.Unlock() 78 | cm.clientBuilds.builds[filePath] = build 79 | } 80 | 81 | func (cm *Manager) RemoveClientBuild(filePath string) { 82 | cm.clientBuilds.lock.Lock() 83 | defer cm.clientBuilds.lock.Unlock() 84 | if _, ok := cm.clientBuilds.builds[filePath]; !ok { 85 | return 86 | } 87 | delete(cm.clientBuilds.builds, filePath) 88 | } 89 | 90 | type routeIDToParentFile struct { 91 | reactFiles map[string]string 92 | lock sync.RWMutex 93 | } 94 | 95 | func (cm *Manager) SetParentFile(routeID, filePath string) { 96 | cm.routeIDToParentFile.lock.Lock() 97 | defer cm.routeIDToParentFile.lock.Unlock() 98 | cm.routeIDToParentFile.reactFiles[routeID] = filePath 99 | } 100 | 101 | func (cm *Manager) GetRouteIDSForParentFile(filePath string) []string { 102 | cm.routeIDToParentFile.lock.RLock() 103 | defer cm.routeIDToParentFile.lock.RUnlock() 104 | var routes []string 105 | for route, file := range cm.routeIDToParentFile.reactFiles { 106 | if file == filePath { 107 | routes = append(routes, route) 108 | } 109 | } 110 | return routes 111 | } 112 | 113 | func (cm *Manager) GetAllRouteIDS() []string { 114 | cm.routeIDToParentFile.lock.RLock() 115 | defer cm.routeIDToParentFile.lock.RUnlock() 116 | routes := make([]string, 0, len(cm.routeIDToParentFile.reactFiles)) 117 | for route := range cm.routeIDToParentFile.reactFiles { 118 | routes = append(routes, route) 119 | } 120 | return routes 121 | } 122 | 123 | func (cm *Manager) GetRouteIDSWithFile(filePath string) []string { 124 | reactFilesWithDependency := cm.GetParentFilesFromDependency(filePath) 125 | if len(reactFilesWithDependency) == 0 { 126 | reactFilesWithDependency = []string{filePath} 127 | } 128 | var routeIDS []string 129 | for _, reactFile := range reactFilesWithDependency { 130 | routeIDS = append(routeIDS, cm.GetRouteIDSForParentFile(reactFile)...) 131 | } 132 | return routeIDS 133 | } 134 | 135 | type parentFileToDependencies struct { 136 | dependencies map[string][]string 137 | lock sync.RWMutex 138 | } 139 | 140 | func (cm *Manager) SetParentFileDependencies(filePath string, dependencies []string) { 141 | cm.parentFileToDependencies.lock.Lock() 142 | defer cm.parentFileToDependencies.lock.Unlock() 143 | cm.parentFileToDependencies.dependencies[filePath] = dependencies 144 | } 145 | 146 | func (cm *Manager) GetParentFilesFromDependency(dependencyPath string) []string { 147 | cm.parentFileToDependencies.lock.RLock() 148 | defer cm.parentFileToDependencies.lock.RUnlock() 149 | var parentFilePaths []string 150 | for parentFilePath, dependencies := range cm.parentFileToDependencies.dependencies { 151 | for _, dependency := range dependencies { 152 | if dependency == dependencyPath { 153 | parentFilePaths = append(parentFilePaths, parentFilePath) 154 | } 155 | } 156 | } 157 | return parentFilePaths 158 | } 159 | -------------------------------------------------------------------------------- /internal/html/base_template.go: -------------------------------------------------------------------------------- 1 | package html 2 | 3 | const BaseTemplate = ` 4 | 5 | 6 | 7 | 8 | {{ .Title }} 9 | {{range $k, $v := .MetaTags}} {{end}} 10 | {{range $k, $v := .OGMetaTags}} {{end}} 11 | {{range .Links}}{{end}} 12 | 13 | 16 | 17 | 18 |
{{ .ServerHTML }}
19 | 26 | 33 | {{if .IsDev}} 34 | 47 | {{end}} 48 | 49 | 50 | ` 51 | -------------------------------------------------------------------------------- /internal/html/error_template.go: -------------------------------------------------------------------------------- 1 | package html 2 | 3 | const ErrorTemplate = ` 4 | 5 | 6 | 7 | 8 | An error occured! 9 | 10 | 18 | 19 | 20 |

An error occured

21 | {{ .Error }} 22 | {{if .IsDev}} 23 | 36 | {{end}} 37 | 38 | 39 | ` 40 | -------------------------------------------------------------------------------- /internal/html/render_html.go: -------------------------------------------------------------------------------- 1 | package html 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "html/template" 7 | "os" 8 | "runtime" 9 | "strings" 10 | ) 11 | 12 | type Params struct { 13 | Title string 14 | MetaTags map[string]string 15 | OGMetaTags map[string]string 16 | Links []struct { 17 | Href string 18 | Rel string 19 | Media string 20 | Hreflang string 21 | Type string 22 | Title string 23 | } 24 | JS template.JS 25 | CSS template.CSS 26 | RouteID string 27 | IsDev bool 28 | ServerHTML template.HTML 29 | } 30 | 31 | // RenderHTMLString Renders the HTML template in internal/html with the given parameters 32 | func RenderHTMLString(params Params) []byte { 33 | params.IsDev = os.Getenv("APP_ENV") != "production" 34 | params.OGMetaTags = getOGMetaTags(params.MetaTags) 35 | params.MetaTags = getMetaTags(params.MetaTags) 36 | t := template.Must(template.New("").Parse(BaseTemplate)) 37 | var output bytes.Buffer 38 | err := t.Execute(&output, params) 39 | if err != nil { 40 | return RenderError(err, params.RouteID) 41 | } 42 | return output.Bytes() 43 | } 44 | 45 | func getMetaTags(metaTags map[string]string) map[string]string { 46 | newMetaTags := make(map[string]string) 47 | for key, value := range metaTags { 48 | if !strings.HasPrefix(key, "og:") { 49 | newMetaTags[key] = value 50 | } 51 | } 52 | return newMetaTags 53 | } 54 | 55 | func getOGMetaTags(metaTags map[string]string) map[string]string { 56 | newMetaTags := make(map[string]string) 57 | for key, value := range metaTags { 58 | if strings.HasPrefix(key, "og:") { 59 | newMetaTags[key] = value 60 | } 61 | } 62 | return newMetaTags 63 | } 64 | 65 | type ErrorParams struct { 66 | Error string 67 | RouteID string 68 | IsDev bool 69 | } 70 | 71 | // RenderError Renders the error template with the given error 72 | func RenderError(e error, routeID string) []byte { 73 | t := template.Must(template.New("").Parse(ErrorTemplate)) 74 | var output bytes.Buffer 75 | _, filename, line, _ := runtime.Caller(1) 76 | t.Execute(&output, ErrorParams{ 77 | Error: fmt.Sprintf("%s line %d: %v", filename, line, e), 78 | RouteID: routeID, 79 | IsDev: os.Getenv("APP_ENV") != "production", 80 | }) 81 | return output.Bytes() 82 | } 83 | -------------------------------------------------------------------------------- /internal/reactbuilder/build.go: -------------------------------------------------------------------------------- 1 | package reactbuilder 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/buger/jsonparser" 9 | esbuildApi "github.com/evanw/esbuild/pkg/api" 10 | "github.com/natewong1313/go-react-ssr/internal/utils" 11 | ) 12 | 13 | var loaders = map[string]esbuildApi.Loader{ 14 | ".png": esbuildApi.LoaderFile, 15 | ".svg": esbuildApi.LoaderFile, 16 | ".jpg": esbuildApi.LoaderFile, 17 | ".jpeg": esbuildApi.LoaderFile, 18 | ".gif": esbuildApi.LoaderFile, 19 | ".bmp": esbuildApi.LoaderFile, 20 | ".woff2": esbuildApi.LoaderFile, 21 | ".woff": esbuildApi.LoaderFile, 22 | ".ttf": esbuildApi.LoaderFile, 23 | ".eot": esbuildApi.LoaderFile, 24 | } 25 | 26 | var textEncoderPolyfill = `function TextEncoder(){}TextEncoder.prototype.encode=function(string){var octets=[];var length=string.length;var i=0;while(i>c));c-=6;while(c>=0){octets.push(0x80|((codePoint>>c)&0x3F));c-=6}i+=codePoint>=0x10000?2:1}return octets};function TextDecoder(){}TextDecoder.prototype.decode=function(octets){var string="";var i=0;while(i0){var k=0;while(k 0 { 84 | fileLocation := "unknown" 85 | lineNum := "unknown" 86 | if result.Errors[0].Location != nil { 87 | fileLocation = result.Errors[0].Location.File 88 | lineNum = result.Errors[0].Location.LineText 89 | } 90 | return BuildResult{}, fmt.Errorf("%s
in %s
at %s", result.Errors[0].Text, fileLocation, lineNum) 91 | } 92 | 93 | var br BuildResult 94 | for _, file := range result.OutputFiles { 95 | if strings.HasSuffix(file.Path, "stdin.js") { 96 | br.JS = string(file.Contents) 97 | } else if strings.HasSuffix(file.Path, "stdin.css") { 98 | br.CSS = string(file.Contents) 99 | } 100 | } 101 | if isClient { 102 | br.Dependencies = getDependencyPathsFromMetafile(result.Metafile) 103 | } 104 | return br, nil 105 | } 106 | 107 | // getDependencyPathsFromMetafile parses dependencies from esbuild metafile and returns the paths of the dependencies 108 | func getDependencyPathsFromMetafile(metafile string) []string { 109 | var dependencyPaths []string 110 | // Parse the metafile and get the paths of the dependencies 111 | // Ignore dependencies in node_modules 112 | err := jsonparser.ObjectEach([]byte(metafile), func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error { 113 | if !strings.Contains(string(key), "/node_modules/") { 114 | dependencyPaths = append(dependencyPaths, utils.GetFullFilePath(string(key))) 115 | } 116 | return nil 117 | }, "inputs") 118 | if err != nil { 119 | return nil 120 | } 121 | return dependencyPaths 122 | } 123 | -------------------------------------------------------------------------------- /internal/reactbuilder/contents.go: -------------------------------------------------------------------------------- 1 | package reactbuilder 2 | 3 | import ( 4 | "strings" 5 | "text/template" 6 | ) 7 | 8 | var baseTemplate = ` 9 | import React from "react"; 10 | {{range $import := .Imports}}{{$import}} {{end}} 11 | import App from "{{ .FilePath }}"; 12 | {{ if .SuppressConsoleLog }}console.log = () => {};{{ end }} 13 | {{ .RenderFunction }}` 14 | var serverRenderFunction = `renderToString();` 15 | var serverRenderFunctionWithLayout = `renderToString();` 16 | var clientRenderFunction = `hydrateRoot(document.getElementById("root"), );` 17 | var clientRenderFunctionWithLayout = `hydrateRoot(document.getElementById("root"), );` 18 | 19 | func buildWithTemplate(buildTemplate string, params map[string]interface{}) (string, error) { 20 | templ, err := template.New("buildTemplate").Parse(buildTemplate) 21 | if err != nil { 22 | return "", err 23 | } 24 | var out strings.Builder 25 | err = templ.Execute(&out, params) 26 | if err != nil { 27 | return "", err 28 | } 29 | return out.String(), nil 30 | } 31 | 32 | func GenerateServerBuildContents(imports []string, filePath string, useLayout bool) (string, error) { 33 | imports = append(imports, `import { renderToString } from "react-dom/server.browser";`) 34 | params := map[string]interface{}{ 35 | "Imports": imports, 36 | "FilePath": filePath, 37 | "RenderFunction": serverRenderFunction, 38 | "SuppressConsoleLog": true, 39 | } 40 | if useLayout { 41 | params["RenderFunction"] = serverRenderFunctionWithLayout 42 | } 43 | return buildWithTemplate(baseTemplate, params) 44 | } 45 | 46 | func GenerateClientBuildContents(imports []string, filePath string, useLayout bool) (string, error) { 47 | imports = append(imports, `import { hydrateRoot } from "react-dom/client";`) 48 | params := map[string]interface{}{ 49 | "Imports": imports, 50 | "FilePath": filePath, 51 | "RenderFunction": clientRenderFunction, 52 | } 53 | if useLayout { 54 | params["RenderFunction"] = clientRenderFunctionWithLayout 55 | } 56 | return buildWithTemplate(baseTemplate, params) 57 | } 58 | -------------------------------------------------------------------------------- /internal/typeconverter/create_temp_file.go: -------------------------------------------------------------------------------- 1 | package typeconverter 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path/filepath" 7 | "strings" 8 | "text/template" 9 | 10 | "github.com/natewong1313/go-react-ssr/internal/utils" 11 | ) 12 | 13 | // https://github.com/tkrajina/typescriptify-golang-structs/blob/master/tscriptify/main.go#L139 14 | func createTemporaryFile(structsFilePath, generatedTypesPath, cacheDir string, structNames []string) (string, error) { 15 | temporaryFilePath := filepath.ToSlash(filepath.Join(cacheDir, "generator.go")) 16 | file, err := os.Create(temporaryFilePath) 17 | if err != nil { 18 | return temporaryFilePath, err 19 | } 20 | defer file.Close() 21 | 22 | t := template.Must(template.New("").Parse(TEMPLATE)) 23 | 24 | structsArr := make([]string, 0) 25 | for _, structName := range structNames { 26 | structName = strings.TrimSpace(structName) 27 | if len(structName) > 0 { 28 | structsArr = append(structsArr, "m."+structName) 29 | } 30 | } 31 | 32 | var params TemplateParams 33 | params.Structs = structsArr 34 | 35 | params.ModuleName, err = getModuleName(structsFilePath) 36 | if err != nil { 37 | return temporaryFilePath, err 38 | } 39 | params.Interface = true 40 | params.TargetFile = utils.GetFullFilePath(generatedTypesPath) 41 | 42 | err = t.Execute(file, params) 43 | if err != nil { 44 | return temporaryFilePath, err 45 | } 46 | 47 | return temporaryFilePath, nil 48 | } 49 | 50 | // getModuleName gets the module name of the props structs file 51 | func getModuleName(propsStructsPath string) (string, error) { 52 | dir := filepath.ToSlash(filepath.Dir(utils.GetFullFilePath(propsStructsPath))) 53 | cmd := exec.Command("go", "list") 54 | cmd.Dir = dir 55 | output, err := cmd.CombinedOutput() 56 | if err != nil { 57 | return "", err 58 | } 59 | return strings.TrimSpace(string(output)), nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/typeconverter/get_structs.go: -------------------------------------------------------------------------------- 1 | package typeconverter 2 | 3 | import ( 4 | "go/ast" 5 | "go/parser" 6 | "go/token" 7 | "os" 8 | ) 9 | 10 | // https://gist.github.com/LukaGiorgadze/570a89a5c3c6d006120da8c29f6684ee 11 | func getStructNamesFromFile(filePath string) (structs []string, err error) { 12 | data, err := os.ReadFile(filePath) 13 | if err != nil { 14 | return 15 | } 16 | fset := token.NewFileSet() 17 | f, err := parser.ParseFile(fset, "", string(data), 0) 18 | if err != nil { 19 | return 20 | } 21 | for _, decl := range f.Decls { 22 | gen, ok := decl.(*ast.GenDecl) 23 | if !ok || gen.Tok != token.TYPE { 24 | continue 25 | } 26 | for _, spec := range gen.Specs { 27 | ts, ok := spec.(*ast.TypeSpec) 28 | if !ok { 29 | continue 30 | } 31 | structs = append(structs, ts.Name.Name) 32 | } 33 | } 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /internal/typeconverter/models.go: -------------------------------------------------------------------------------- 1 | package typeconverter 2 | 3 | import "strings" 4 | 5 | const TEMPLATE = `package main 6 | 7 | import ( 8 | m "{{ .ModuleName }}" 9 | "github.com/tkrajina/typescriptify-golang-structs/typescriptify" 10 | ) 11 | 12 | func main() { 13 | t := typescriptify.New() 14 | t.CreateInterface = {{ .Interface }} 15 | t.BackupDir = "" 16 | {{ range .Structs }} t.Add({{ . }}{}) 17 | {{ end }} 18 | {{ range .CustomImports }} t.AddImport("{{ . }}") 19 | {{ end }} 20 | err := t.ConvertToFile(` + "`{{ .TargetFile }}`" + `) 21 | if err != nil { 22 | panic(err.Error()) 23 | } 24 | }` 25 | 26 | type TemplateParams struct { 27 | ModuleName string 28 | TargetFile string 29 | Structs []string 30 | InitParams map[string]interface{} 31 | CustomImports arrayImports 32 | Interface bool 33 | Verbose bool 34 | } 35 | 36 | type arrayImports []string 37 | 38 | func (i *arrayImports) String() string { 39 | return "// custom imports:\n\n" + strings.Join(*i, "\n") 40 | } 41 | 42 | func (i *arrayImports) Set(value string) error { 43 | *i = append(*i, value) 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /internal/typeconverter/start.go: -------------------------------------------------------------------------------- 1 | package typeconverter 2 | 3 | import ( 4 | "github.com/natewong1313/go-react-ssr/internal/utils" 5 | "os/exec" 6 | 7 | _ "github.com/tkrajina/typescriptify-golang-structs/typescriptify" 8 | ) 9 | 10 | // Start starts the type converter 11 | // It gets the name of structs in PropsStructsPath and generates a temporary file to run the type converter 12 | func Start(structsFilePath, generatedTypesPath string) error { 13 | // Get struct names from file 14 | structNames, err := getStructNamesFromFile(structsFilePath) 15 | if err != nil { 16 | return err 17 | } 18 | // Create a folder for the temporary generator files 19 | cacheDir, err := utils.GetTypeConverterCacheDir() 20 | if err != nil { 21 | return err 22 | } 23 | // Create the generator file 24 | temporaryFilePath, err := createTemporaryFile(structsFilePath, generatedTypesPath, cacheDir, structNames) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | // Run the file 30 | cmd := exec.Command("go", "run", temporaryFilePath) 31 | _, err = cmd.CombinedOutput() 32 | if err != nil { 33 | return err 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/utils/cachedir.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | func CleanCacheDirectories() { 9 | cacheDir, err := createCacheDirIfNotExists() 10 | if err != nil { 11 | return 12 | } 13 | typeConverterCacheDir, _ := GetTypeConverterCacheDir() 14 | if err := os.RemoveAll(typeConverterCacheDir); err != nil { 15 | return 16 | } 17 | cssCacheDir, _ := GetCSSCacheDir() 18 | if err := os.RemoveAll(cssCacheDir); err != nil { 19 | return 20 | } 21 | // Remove GetServerBuildCacheDir 22 | if err := os.RemoveAll(filepath.Join(cacheDir, "builds")); err != nil { 23 | return 24 | } 25 | } 26 | 27 | // createCacheDirIfNotExists creates the user cache directory if it doesn't exist 28 | func createCacheDirIfNotExists() (string, error) { 29 | userCacheDir, err := os.UserCacheDir() 30 | if err != nil { 31 | return "", err 32 | } 33 | gossrCacheDirPath := filepath.Join(userCacheDir, "gossr") 34 | err = os.MkdirAll(gossrCacheDirPath, os.ModePerm) 35 | return gossrCacheDirPath, err 36 | } 37 | 38 | // GetTypeConverterCacheDir returns the path to the type converter cache directory 39 | func GetTypeConverterCacheDir() (string, error) { 40 | cacheDir, err := createCacheDirIfNotExists() 41 | if err != nil { 42 | return "", err 43 | } 44 | typeConverterCacheDir := filepath.Join(cacheDir, "typeconverter") 45 | err = os.MkdirAll(typeConverterCacheDir, os.ModePerm) 46 | return typeConverterCacheDir, err 47 | } 48 | 49 | // GetServerBuildCacheDir returns the path to the server build cache directory for the given route 50 | func GetServerBuildCacheDir(fileName string) (string, error) { 51 | cacheDir, err := createCacheDirIfNotExists() 52 | if err != nil { 53 | return "", err 54 | } 55 | serverBuildCacheDir := filepath.Join(cacheDir, "builds") 56 | err = os.MkdirAll(serverBuildCacheDir, os.ModePerm) 57 | if err != nil { 58 | return "", err 59 | } 60 | routeCacheDir := filepath.Join(serverBuildCacheDir, fileName) 61 | err = os.MkdirAll(routeCacheDir, os.ModePerm) 62 | return routeCacheDir, err 63 | } 64 | 65 | // GetCSSCacheDir returns the path to the server build cache directory for the given route 66 | func GetCSSCacheDir() (string, error) { 67 | cacheDir, err := createCacheDirIfNotExists() 68 | if err != nil { 69 | return "", err 70 | } 71 | cssCacheDir := filepath.Join(cacheDir, "css_builds") 72 | err = os.MkdirAll(cssCacheDir, os.ModePerm) 73 | return cssCacheDir, err 74 | } 75 | 76 | // GetTailwindExecutableDir returns the path to the tailwind executable directory 77 | func GetTailwindExecutableDir() (string, error) { 78 | cacheDir, err := createCacheDirIfNotExists() 79 | if err != nil { 80 | return "", err 81 | } 82 | tailwindCacheDir := filepath.Join(cacheDir, "tailwind") 83 | err = os.MkdirAll(tailwindCacheDir, os.ModePerm) 84 | return tailwindCacheDir, err 85 | } 86 | -------------------------------------------------------------------------------- /internal/utils/filepath.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "path/filepath" 4 | 5 | // Returns the absolute path of the file without returning an error 6 | func GetFullFilePath(filePath string) string { 7 | fp, _ := filepath.Abs(filePath) 8 | return filepath.ToSlash(fp) 9 | } 10 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package go_ssr 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/natewong1313/go-react-ssr/internal/html" 7 | "github.com/natewong1313/go-react-ssr/internal/utils" 8 | "github.com/rs/zerolog" 9 | "html/template" 10 | "os" 11 | "path/filepath" 12 | "runtime" 13 | ) 14 | 15 | // RenderConfig is the config for rendering a route 16 | type RenderConfig struct { 17 | File string 18 | Title string 19 | MetaTags map[string]string 20 | Props interface{} 21 | } 22 | 23 | // RenderRoute renders a route to html 24 | func (engine *Engine) RenderRoute(renderConfig RenderConfig) []byte { 25 | // routeID is the program counter of the caller 26 | pc, _, _, _ := runtime.Caller(1) 27 | routeID := fmt.Sprint(pc) 28 | 29 | props, err := propsToString(renderConfig.Props) 30 | if err != nil { 31 | return html.RenderError(err, routeID) 32 | } 33 | task := renderTask{ 34 | engine: engine, 35 | logger: zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger(), 36 | routeID: routeID, 37 | props: props, 38 | filePath: filepath.ToSlash(utils.GetFullFilePath(engine.Config.FrontendDir + "/" + renderConfig.File)), 39 | config: renderConfig, 40 | } 41 | renderedHTML, css, js, err := task.Start() 42 | if err != nil { 43 | return html.RenderError(err, task.routeID) 44 | } 45 | return html.RenderHTMLString(html.Params{ 46 | Title: renderConfig.Title, 47 | MetaTags: renderConfig.MetaTags, 48 | JS: template.JS(js), 49 | CSS: template.CSS(css), 50 | RouteID: task.routeID, 51 | ServerHTML: template.HTML(renderedHTML), 52 | }) 53 | } 54 | 55 | // Convert props to JSON string, or set to null if no props are passed 56 | func propsToString(props interface{}) (string, error) { 57 | if props != nil { 58 | propsJSON, err := json.Marshal(props) 59 | return string(propsJSON), err 60 | } 61 | return "null", nil 62 | } 63 | -------------------------------------------------------------------------------- /rendertask.go: -------------------------------------------------------------------------------- 1 | package go_ssr 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/buke/quickjs-go" 7 | "github.com/natewong1313/go-react-ssr/internal/reactbuilder" 8 | "github.com/rs/zerolog" 9 | ) 10 | 11 | type renderTask struct { 12 | engine *Engine 13 | logger zerolog.Logger 14 | routeID string 15 | filePath string 16 | props string 17 | config RenderConfig 18 | serverRenderResult chan serverRenderResult 19 | clientRenderResult chan clientRenderResult 20 | } 21 | 22 | type serverRenderResult struct { 23 | html string 24 | css string 25 | err error 26 | } 27 | 28 | type clientRenderResult struct { 29 | js string 30 | dependencies []string 31 | err error 32 | } 33 | 34 | // Start starts the render task, returns the rendered html, css, and js for hydration 35 | func (rt *renderTask) Start() (string, string, string, error) { 36 | rt.serverRenderResult = make(chan serverRenderResult) 37 | rt.clientRenderResult = make(chan clientRenderResult) 38 | // Assigns the parent file to the routeID so that the cache can be invalidated when the parent file changes 39 | rt.engine.CacheManager.SetParentFile(rt.routeID, rt.filePath) 40 | 41 | // Render for server and client concurrently 42 | go rt.doRender("server") 43 | go rt.doRender("client") 44 | 45 | // Wait for both to finish 46 | srResult := <-rt.serverRenderResult 47 | if srResult.err != nil { 48 | rt.logger.Error().Err(srResult.err).Msg("Failed to build for server") 49 | return "", "", "", srResult.err 50 | } 51 | crResult := <-rt.clientRenderResult 52 | if crResult.err != nil { 53 | rt.logger.Error().Err(crResult.err).Msg("Failed to build for client") 54 | return "", "", "", crResult.err 55 | } 56 | 57 | // Set the parent file dependencies so that the cache can be invalidated a dependency changes 58 | go rt.engine.CacheManager.SetParentFileDependencies(rt.filePath, crResult.dependencies) 59 | return srResult.html, srResult.css, crResult.js, nil 60 | } 61 | 62 | func (rt *renderTask) doRender(buildType string) { 63 | // Check if the build is in the cache 64 | build, buildFound := rt.getBuildFromCache(buildType) 65 | if !buildFound { 66 | // Build the file if it's not in the cache 67 | newBuild, err := rt.buildFile(buildType) 68 | if err != nil { 69 | rt.handleBuildError(err, buildType) 70 | return 71 | } 72 | rt.updateBuildCache(newBuild, buildType) 73 | build = newBuild 74 | } 75 | // JS is built without props so that the props can be injected into cached JS builds 76 | js := injectProps(build.JS, rt.props) 77 | if buildType == "server" { 78 | // Then call that file with node to get the rendered HTML 79 | renderedHTML, err := renderReactToHTMLNew(js) 80 | rt.serverRenderResult <- serverRenderResult{html: renderedHTML, css: build.CSS, err: err} 81 | } else { 82 | rt.clientRenderResult <- clientRenderResult{js: js, dependencies: build.Dependencies} 83 | } 84 | } 85 | 86 | // getBuild returns the build from the cache if it exists 87 | func (rt *renderTask) getBuildFromCache(buildType string) (reactbuilder.BuildResult, bool) { 88 | if buildType == "server" { 89 | return rt.engine.CacheManager.GetServerBuild(rt.filePath) 90 | } else { 91 | return rt.engine.CacheManager.GetClientBuild(rt.filePath) 92 | } 93 | } 94 | 95 | // buildFile gets the contents of the file to be built and builds it with reactbuilder 96 | func (rt *renderTask) buildFile(buildType string) (reactbuilder.BuildResult, error) { 97 | buildContents, err := rt.getBuildContents(buildType) 98 | if err != nil { 99 | return reactbuilder.BuildResult{}, err 100 | } 101 | if buildType == "server" { 102 | return reactbuilder.BuildServer(buildContents, rt.engine.Config.FrontendDir, rt.engine.Config.AssetRoute) 103 | } else { 104 | return reactbuilder.BuildClient(buildContents, rt.engine.Config.FrontendDir, rt.engine.Config.AssetRoute) 105 | } 106 | } 107 | 108 | // getBuildContents gets the required imports based on the config and returns the contents to be built with reactbuilder 109 | func (rt *renderTask) getBuildContents(buildType string) (string, error) { 110 | var imports []string 111 | if rt.engine.CachedLayoutCSSFilePath != "" { 112 | imports = append(imports, fmt.Sprintf(`import "%s";`, rt.engine.CachedLayoutCSSFilePath)) 113 | } 114 | if rt.engine.Config.LayoutFilePath != "" { 115 | imports = append(imports, fmt.Sprintf(`import Layout from "%s";`, rt.engine.Config.LayoutFilePath)) 116 | } 117 | if buildType == "server" { 118 | return reactbuilder.GenerateServerBuildContents(imports, rt.filePath, rt.engine.Config.LayoutFilePath != "") 119 | } else { 120 | return reactbuilder.GenerateClientBuildContents(imports, rt.filePath, rt.engine.Config.LayoutFilePath != "") 121 | } 122 | } 123 | 124 | // handleBuildError handles the error from building the file and sends it to the appropriate channel 125 | func (rt *renderTask) handleBuildError(err error, buildType string) { 126 | if buildType == "server" { 127 | rt.serverRenderResult <- serverRenderResult{err: err} 128 | } else { 129 | rt.clientRenderResult <- clientRenderResult{err: err} 130 | } 131 | } 132 | 133 | // updateBuildCache updates the cache with the new build 134 | func (rt *renderTask) updateBuildCache(build reactbuilder.BuildResult, buildType string) { 135 | if buildType == "server" { 136 | rt.engine.CacheManager.SetServerBuild(rt.filePath, build) 137 | } else { 138 | rt.engine.CacheManager.SetClientBuild(rt.filePath, build) 139 | } 140 | } 141 | 142 | // injectProps injects the props into the already compiled JS 143 | func injectProps(compiledJS, props string) string { 144 | return fmt.Sprintf(`var props = %s; %s`, props, compiledJS) 145 | } 146 | 147 | // renderReactToHTML uses node to execute the server js file which outputs the rendered HTML 148 | func renderReactToHTMLNew(js string) (string, error) { 149 | rt := quickjs.NewRuntime() 150 | defer rt.Close() 151 | ctx := rt.NewContext() 152 | defer ctx.Close() 153 | res, err := ctx.Eval(js) 154 | if err != nil { 155 | return "", err 156 | } 157 | return res.String(), nil 158 | } 159 | --------------------------------------------------------------------------------