initial
This commit is contained in:
34
Dockerfile
Normal file
34
Dockerfile
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
## Build
|
||||||
|
FROM golang:1.22-alpine AS build
|
||||||
|
|
||||||
|
ARG VERSION='dev'
|
||||||
|
|
||||||
|
RUN apk update && apk add --no-cache curl
|
||||||
|
|
||||||
|
RUN curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 \
|
||||||
|
&& chmod +x tailwindcss-linux-x64 \
|
||||||
|
&& mv tailwindcss-linux-x64 /usr/local/bin/tailwindcss
|
||||||
|
|
||||||
|
RUN go install github.com/a-h/templ/cmd/templ@v0.2.663 \
|
||||||
|
&& go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY ./ /app
|
||||||
|
|
||||||
|
RUN templ generate -path ./components \
|
||||||
|
&& tailwindcss -i ./styles/input.css -o ./dist/assets/css/output@${VERSION}.css --minify \
|
||||||
|
&& sqlc generate
|
||||||
|
|
||||||
|
RUN go build -ldflags="-s -w -X version.Value=${VERSION}" -o my-app
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
FROM gcr.io/distroless/static-debian12
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY --from=build /app/my-app /my-app
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["/my-app"]
|
21
Makefile
Normal file
21
Makefile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
format-templ:
|
||||||
|
@echo "Formatting templ files..."
|
||||||
|
@templ fmt .
|
||||||
|
generate-templ:
|
||||||
|
@echo "Generating templ files..."
|
||||||
|
@templ generate -path ./components
|
||||||
|
generate-templ-watch:
|
||||||
|
@echo "Generating templ files..."
|
||||||
|
@templ generate -path ./components -watch
|
||||||
|
generate-tailwind:
|
||||||
|
@echo "Generating tailwind files..."
|
||||||
|
@tailwindcss -i ./styles/input.css -o ./dist/assets/css/output@dev.css
|
||||||
|
generate-tailwind-watch:
|
||||||
|
@echo "Generating tailwind files..."
|
||||||
|
@tailwindcss -i ./styles/input.css -o ./dist/assets/css/output@dev.css --watch
|
||||||
|
run:
|
||||||
|
@echo "Running..."
|
||||||
|
@go run main.go
|
||||||
|
build:
|
||||||
|
@echo "Building..."
|
||||||
|
@go build -o ./app -ldflags="-s -w -X version.Value=1.0.0"
|
242
README.md
Normal file
242
README.md
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
# Go + HTMX Template
|
||||||
|
|
||||||
|
This is a template repository that comes with everything you need to build a Web Application using Go (templ) and HTMX.
|
||||||
|
|
||||||
|
The template comes with a basic structure of using a SQL DB (`sqlc`), E2E testing (playwright), and styling (tailwindcss).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
In the top right, select the dropdown __Use this template__ and select __Create a new repository__.
|
||||||
|
|
||||||
|
Once cloned, run the `update_module.sh` script to change the module to your module name.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./update_module vctpule
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technologies
|
||||||
|
|
||||||
|
A few different technologies are configured to help getting off the ground easier.
|
||||||
|
|
||||||
|
- [sqlc](https://sqlc.dev/) for database layer
|
||||||
|
- Stubbed to use SQLite
|
||||||
|
- This can be easily swapped with [sqlx](https://jmoiron.github.io/sqlx/)
|
||||||
|
- The script `upgrade_sqlc.sh` is available to upgrade GitHub Workflow files to latest sqlc version
|
||||||
|
- [Tailwind CSS](https://tailwindcss.com/) for styling
|
||||||
|
- Output is generated with the [CLI](https://tailwindcss.com/docs/installation)
|
||||||
|
- [templ](https://templ.guide/) for creating HTML
|
||||||
|
- The script `upgrade_templ.sh` is available to make upgrading easier
|
||||||
|
- [HTMX](https://htmx.org/) for HTML interaction
|
||||||
|
- The script `upgrade_htmx.sh` is available to make upgrading easier
|
||||||
|
- [air](https://github.com/cosmtrek/air) for live reloading of the application.
|
||||||
|
- [golang migrate](https://github.com/golang-migrate/migrate) for DB migrations.
|
||||||
|
- [playwright-go](https://github.com/playwright-community/playwright-go) for E2E testing.
|
||||||
|
|
||||||
|
Everything else uses the standard library.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── Makefile
|
||||||
|
├── components
|
||||||
|
│ ├── core
|
||||||
|
│ │ └── html.templ
|
||||||
|
│ └── home
|
||||||
|
│ └── home.templ
|
||||||
|
├── db
|
||||||
|
│ ├── db.go
|
||||||
|
│ ├── local.go
|
||||||
|
│ ├── migrations
|
||||||
|
│ │ ├── 20240407203525_init.down.sql
|
||||||
|
│ │ └── 20240407203525_init.up.sql
|
||||||
|
│ └── queries
|
||||||
|
│ └── query.sql
|
||||||
|
├── db.sqlite3
|
||||||
|
├── dist
|
||||||
|
│ ├── assets
|
||||||
|
│ │ └── js
|
||||||
|
│ │ └── htmx@1.9.10.min.js
|
||||||
|
│ └── dist.go
|
||||||
|
├── e2e
|
||||||
|
│ ├── e2e_test.go
|
||||||
|
│ ├── home_test.go
|
||||||
|
│ └── testdata
|
||||||
|
│ └── seed.sql
|
||||||
|
├── go.mod
|
||||||
|
├── go.sum
|
||||||
|
├── log
|
||||||
|
│ └── log.go
|
||||||
|
├── main.go
|
||||||
|
├── server
|
||||||
|
│ ├── handler
|
||||||
|
│ │ ├── handler.go
|
||||||
|
│ │ └── home.go
|
||||||
|
│ ├── middleware
|
||||||
|
│ │ ├── cache.go
|
||||||
|
│ │ ├── logging.go
|
||||||
|
│ │ └── middleware.go
|
||||||
|
│ ├── router
|
||||||
|
│ │ └── router.go
|
||||||
|
│ └── server.go
|
||||||
|
├── sqlc.yml
|
||||||
|
├── styles
|
||||||
|
│ └── input.css
|
||||||
|
├── tailwind.config.js
|
||||||
|
└── version
|
||||||
|
└── version.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
This is where `templ` files live. Anything you want to render to the user goes here. Note, all
|
||||||
|
`*.go` files will be ignored by `git` (configured in `.gitignore`).
|
||||||
|
|
||||||
|
### DB
|
||||||
|
|
||||||
|
This is the directory that `sqlc` generates to. Update `queries.sql` to build
|
||||||
|
your database operations.
|
||||||
|
|
||||||
|
This project uses [golang migrate](https://github.com/golang-migrate/migrate) for DB
|
||||||
|
migrations. `sqlc` uses the `db/migrations` directory to generating DB tables. Call
|
||||||
|
`db.Migrate(..)` to automatically migrate your database to the latest version. To add migration
|
||||||
|
call the following command,
|
||||||
|
|
||||||
|
```shell
|
||||||
|
migrate create -ext sql -dir db/migrations <name of migration>
|
||||||
|
```
|
||||||
|
|
||||||
|
This package can be easily update to use `sqlx` as well.
|
||||||
|
|
||||||
|
### Dist
|
||||||
|
|
||||||
|
This is where your assets live. Any Javascript, images, or styling needs to go in the
|
||||||
|
`dist/assets` directory. The directory will be embedded into the application.
|
||||||
|
|
||||||
|
Note, the `dist/assets/css` will be ignored by `git` (configured in `.gitignore`) since the
|
||||||
|
files that are written to this directory are done by the Tailwind CSS CLI. Custom styles should
|
||||||
|
go in the `styles/input.css` file.
|
||||||
|
|
||||||
|
### E2E
|
||||||
|
|
||||||
|
To test the UI, the `e2e` directory contains the Go tests for performing End to end testing. To
|
||||||
|
run the tests, run the command
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go test -v ./... -tags=e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
The end to end tests, will start up the app, on a random port, seeding the database using the
|
||||||
|
`seed.sql` file. Once the tests are complete, the app will be stopped.
|
||||||
|
|
||||||
|
The E2E tests use Playwright (Go) for better integration into the Go tooling.
|
||||||
|
|
||||||
|
### Log
|
||||||
|
|
||||||
|
This contains helper function to create a `slog.Logger`. Log level and output type can be set
|
||||||
|
with then environment variables `LOG_LEVEL` and `LOG_OUTPUT`. The logger will write to
|
||||||
|
`stdout`.
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
This contains everything related to the HTTP server. It comes with a graceful shutdown handler
|
||||||
|
that handles `SIGINT`.
|
||||||
|
|
||||||
|
#### Router
|
||||||
|
|
||||||
|
This package sets up the routing for the application, such as the `/assets/` path and `/` path.
|
||||||
|
It uses the standard libraries mux for routing. You can easily swap out for other HTTP
|
||||||
|
routers such as [gorilla/mux](https://github.com/gorilla/mux).
|
||||||
|
|
||||||
|
#### Middleware
|
||||||
|
|
||||||
|
This package contains any middleware to configured with routes.
|
||||||
|
|
||||||
|
#### Handler
|
||||||
|
|
||||||
|
This package contains the handler to handle the actual routes.
|
||||||
|
|
||||||
|
#### Styles
|
||||||
|
|
||||||
|
This contains the `input.css` that the Tailwind CSS CLI uses to generate your output CSS.
|
||||||
|
Update `input.css` with any custom CSS you need and it will be included in the output CSS.
|
||||||
|
|
||||||
|
#### Version
|
||||||
|
|
||||||
|
This package allows you to set a version at build time. If not set, the version defaults to
|
||||||
|
`dev`. To set the version run the following command,
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go build -o ./app -ldflags="-X version.Value=1.0.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
See the `Makefile` for building the application.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
There are a couple builtin ways to run the application - using `air` or the `Makefile` helper
|
||||||
|
commands.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Install [templ](https://templ.guide/quick-start/installation)
|
||||||
|
- Install [sqlc](https://docs.sqlc.dev/en/stable/overview/install.html)
|
||||||
|
- Install [tailwindcss CLI](https://tailwindcss.com/docs/installation)
|
||||||
|
- Install [air](https://github.com/cosmtrek/air#installation)
|
||||||
|
|
||||||
|
### air
|
||||||
|
|
||||||
|
`air` has been configured with the file `.air.toml` to allow live reloading of the application
|
||||||
|
when a file changes.
|
||||||
|
|
||||||
|
To run, install `air`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go install github.com/cosmtrek/air@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Then simply run the command
|
||||||
|
|
||||||
|
```shell
|
||||||
|
air
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Address Already In Use Error
|
||||||
|
|
||||||
|
Sometimes, you may run into the issue _address already in use_. If this is the case, you
|
||||||
|
can run this command to find the PID to kill it.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
ps aux | grep tmp/main
|
||||||
|
```
|
||||||
|
|
||||||
|
### Makefile
|
||||||
|
|
||||||
|
You can also run with the provided `Makefile`. There are commands to generate `templ` files and
|
||||||
|
tailwind output css.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Generate and watch templ
|
||||||
|
make generate-templ-watch
|
||||||
|
|
||||||
|
# Genrate and watch tailwindcss
|
||||||
|
make generate-tailwind-watch
|
||||||
|
|
||||||
|
# Run application
|
||||||
|
make run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Github Workflow
|
||||||
|
|
||||||
|
The repository comes with two Github workflows as well. One called `ci.yml` that lints and
|
||||||
|
tests your code. The other called `release.yml` that creates a tag, GitHub Release, and
|
||||||
|
attaches the Linux binary to the Release.
|
||||||
|
|
||||||
|
Note, the version of `github.com/a-h/templ/cmd/templ` matches the version in `go.mod`. If these
|
||||||
|
do not match, the build will fail. When upgrading your `templ` version, make sure to update
|
||||||
|
`ci.yml` and `release.yml`.
|
||||||
|
|
||||||
|
### GoReleaser
|
||||||
|
|
||||||
|
If you need to compile for more than Linux, see [GoReleaser](https://goreleaser.com/) for a
|
||||||
|
better release process.
|
30
components/core/html.templ
Normal file
30
components/core/html.templ
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import "vctp/version"
|
||||||
|
|
||||||
|
templ HTML(title string, content templ.Component) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
@head(title)
|
||||||
|
@body(content)
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ head(title string) {
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<meta name="description" content="Hello world"/>
|
||||||
|
<title>{ title }</title>
|
||||||
|
<script src="/assets/js/htmx@v2.0.2.min.js"></script>
|
||||||
|
<link href={ "/assets/css/output@" + version.Value + ".css" } rel="stylesheet"/>
|
||||||
|
</head>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ body(content templ.Component) {
|
||||||
|
<body class="flex flex-col min-h-screen">
|
||||||
|
<main class="flex-grow">
|
||||||
|
@content
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
}
|
8
components/home/home.templ
Normal file
8
components/home/home.templ
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package home
|
||||||
|
|
||||||
|
templ Home() {
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-5xl font-bold">Welcome!</h1>
|
||||||
|
<p class="text-indigo-200 mt-4">This is a simple home screen.</p>
|
||||||
|
</div>
|
||||||
|
}
|
55
db/db.go
Normal file
55
db/db.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"vctp/db/queries"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||||
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed migrations/*.sql
|
||||||
|
var migrations embed.FS
|
||||||
|
|
||||||
|
type Database interface {
|
||||||
|
DB() *sql.DB
|
||||||
|
Queries() *queries.Queries
|
||||||
|
Logger() *slog.Logger
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(logger *slog.Logger, url string) (Database, error) {
|
||||||
|
db, err := newLocalDB(logger, url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = db.db.Ping(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate runs the migrations on the database. Assumes the database is SQLite.
|
||||||
|
func Migrate(db Database) error {
|
||||||
|
driver, err := sqlite3.WithInstance(db.DB(), &sqlite3.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create database driver: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
iofsDriver, err := iofs.New(migrations, "migrations")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create iofs: %w", err)
|
||||||
|
}
|
||||||
|
defer iofsDriver.Close()
|
||||||
|
|
||||||
|
m, err := migrate.NewWithInstance("iofs", iofsDriver, "sqlite3", driver)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create migration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.Up()
|
||||||
|
}
|
42
db/local.go
Normal file
42
db/local.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"vctp/db/queries"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
_ "github.com/tursodatabase/libsql-client-go/libsql"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocalDB struct {
|
||||||
|
logger *slog.Logger
|
||||||
|
db *sql.DB
|
||||||
|
queries *queries.Queries
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Database = (*LocalDB)(nil)
|
||||||
|
|
||||||
|
func (d *LocalDB) DB() *sql.DB {
|
||||||
|
return d.db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LocalDB) Queries() *queries.Queries {
|
||||||
|
return d.queries
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LocalDB) Logger() *slog.Logger {
|
||||||
|
return d.logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LocalDB) Close() error {
|
||||||
|
return d.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLocalDB(logger *slog.Logger, path string) (*LocalDB, error) {
|
||||||
|
db, err := sql.Open("libsql", "file:"+path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &LocalDB{logger: logger, db: db, queries: queries.New(db)}, nil
|
||||||
|
}
|
0
db/migrations/20240407203525_init.down.txt
Normal file
0
db/migrations/20240407203525_init.down.txt
Normal file
7
db/migrations/20240407203525_init.up.txt
Normal file
7
db/migrations/20240407203525_init.up.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS authors (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
bio TEXT
|
||||||
|
);
|
||||||
|
|
0
db/migrations/20240912985300_init_up.sql
Normal file
0
db/migrations/20240912985300_init_up.sql
Normal file
25
db/queries/query.sql
Normal file
25
db/queries/query.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
-- name: GetAuthor :one
|
||||||
|
SELECT * FROM authors
|
||||||
|
WHERE id = ? LIMIT 1;
|
||||||
|
|
||||||
|
-- name: ListAuthors :many
|
||||||
|
SELECT * FROM authors
|
||||||
|
ORDER BY name;
|
||||||
|
|
||||||
|
-- name: CreateAuthor :one
|
||||||
|
INSERT INTO authors (
|
||||||
|
name, bio
|
||||||
|
) VALUES (
|
||||||
|
?, ?
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: UpdateAuthor :exec
|
||||||
|
UPDATE authors
|
||||||
|
SET name = ?,
|
||||||
|
bio = ?
|
||||||
|
WHERE id = ?;
|
||||||
|
|
||||||
|
-- name: DeleteAuthor :exec
|
||||||
|
DELETE FROM authors
|
||||||
|
WHERE id = ?;
|
1
dist/assets/js/htmx@v2.0.2.min.js
vendored
Normal file
1
dist/assets/js/htmx@v2.0.2.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
dist/dist.go
vendored
Normal file
8
dist/dist.go
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package dist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed all:assets
|
||||||
|
var AssetsDir embed.FS
|
237
e2e/e2e_test.go
Normal file
237
e2e/e2e_test.go
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
//go:build e2e
|
||||||
|
|
||||||
|
package e2e_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/playwright-community/playwright-go"
|
||||||
|
_ "github.com/tursodatabase/libsql-client-go/libsql"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// global variables, can be used in any tests
|
||||||
|
var (
|
||||||
|
pw *playwright.Playwright
|
||||||
|
browser playwright.Browser
|
||||||
|
context playwright.BrowserContext
|
||||||
|
page playwright.Page
|
||||||
|
expect playwright.PlaywrightAssertions
|
||||||
|
isChromium bool
|
||||||
|
isFirefox bool
|
||||||
|
isWebKit bool
|
||||||
|
browserName = getBrowserName()
|
||||||
|
browserType playwright.BrowserType
|
||||||
|
app *exec.Cmd
|
||||||
|
baseUrL *url.URL
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultContextOptions for most tests
|
||||||
|
var defaultContextOptions = playwright.BrowserNewContextOptions{
|
||||||
|
AcceptDownloads: playwright.Bool(true),
|
||||||
|
HasTouch: playwright.Bool(true),
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
beforeAll()
|
||||||
|
code := m.Run()
|
||||||
|
afterAll()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// beforeAll prepares the environment, including
|
||||||
|
// - start Playwright driver
|
||||||
|
// - launch browser depends on BROWSER env
|
||||||
|
// - init web-first assertions, alias as `expect`
|
||||||
|
func beforeAll() {
|
||||||
|
err := playwright.Install()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not install Playwright: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pw, err = playwright.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not start Playwright: %v", err)
|
||||||
|
}
|
||||||
|
if browserName == "chromium" || browserName == "" {
|
||||||
|
browserType = pw.Chromium
|
||||||
|
} else if browserName == "firefox" {
|
||||||
|
browserType = pw.Firefox
|
||||||
|
} else if browserName == "webkit" {
|
||||||
|
browserType = pw.WebKit
|
||||||
|
}
|
||||||
|
// launch browser, headless or not depending on HEADFUL env
|
||||||
|
browser, err = browserType.Launch(playwright.BrowserTypeLaunchOptions{
|
||||||
|
Headless: playwright.Bool(os.Getenv("HEADFUL") == ""),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not launch: %v", err)
|
||||||
|
}
|
||||||
|
// init web-first assertions with 1s timeout instead of default 5s
|
||||||
|
expect = playwright.NewPlaywrightAssertions(1000)
|
||||||
|
isChromium = browserName == "chromium" || browserName == ""
|
||||||
|
isFirefox = browserName == "firefox"
|
||||||
|
isWebKit = browserName == "webkit"
|
||||||
|
|
||||||
|
// start app
|
||||||
|
if err = startApp(); err != nil {
|
||||||
|
log.Fatalf("could not start app: %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second * 5)
|
||||||
|
if err = seedDB(); err != nil {
|
||||||
|
log.Fatalf("could not seed db: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startApp() error {
|
||||||
|
port := getPort()
|
||||||
|
app = exec.Command("go", "run", "main.go")
|
||||||
|
app.Dir = "../"
|
||||||
|
app.Env = append(
|
||||||
|
os.Environ(),
|
||||||
|
"DB_URL=./test-db.sqlite3",
|
||||||
|
fmt.Sprintf("PORT=%d", port),
|
||||||
|
"LOG_LEVEL=DEBUG",
|
||||||
|
)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
baseUrL, err = url.Parse(fmt.Sprintf("http://localhost:%d", port))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, err := app.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stderr, err := app.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Started app on port %d, pid %d", port, app.Process.Pid)
|
||||||
|
|
||||||
|
stdoutchan := make(chan string)
|
||||||
|
stderrchan := make(chan string)
|
||||||
|
go func() {
|
||||||
|
defer close(stdoutchan)
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
for scanner.Scan() {
|
||||||
|
stdoutchan <- scanner.Text()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer close(stderrchan)
|
||||||
|
scanner := bufio.NewScanner(stderr)
|
||||||
|
for scanner.Scan() {
|
||||||
|
stderrchan <- scanner.Text()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for line := range stdoutchan {
|
||||||
|
fmt.Println("[STDOUT]", line)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
for line := range stderrchan {
|
||||||
|
fmt.Println("[STDERR]", line)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedDB() error {
|
||||||
|
db, err := sql.Open("libsql", "file:../test-db.sqlite3")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err := os.ReadFile("./testdata/seed.sql")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = db.Exec(string(b))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPort() int {
|
||||||
|
randomGenerator := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
return randomGenerator.Intn(9001-3000) + 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
// afterAll does cleanup, e.g. stop playwright driver
|
||||||
|
func afterAll() {
|
||||||
|
if app != nil && app.Process != nil {
|
||||||
|
if err := syscall.Kill(-app.Process.Pid, syscall.SIGKILL); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := pw.Stop(); err != nil {
|
||||||
|
log.Fatalf("could not start Playwright: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Remove("../test-db.sqlite3"); err != nil {
|
||||||
|
log.Fatalf("could not remove test-db.sqlite3: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// beforeEach creates a new context and page for each test,
|
||||||
|
// so each test has isolated environment. Usage:
|
||||||
|
//
|
||||||
|
// Func TestFoo(t *testing.T) {
|
||||||
|
// beforeEach(t)
|
||||||
|
// // your test code
|
||||||
|
// }
|
||||||
|
func beforeEach(t *testing.T, contextOptions ...playwright.BrowserNewContextOptions) {
|
||||||
|
t.Helper()
|
||||||
|
opt := defaultContextOptions
|
||||||
|
if len(contextOptions) == 1 {
|
||||||
|
opt = contextOptions[0]
|
||||||
|
}
|
||||||
|
context, page = newBrowserContextAndPage(t, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBrowserName() string {
|
||||||
|
browserName, hasEnv := os.LookupEnv("BROWSER")
|
||||||
|
if hasEnv {
|
||||||
|
return browserName
|
||||||
|
}
|
||||||
|
return "chromium"
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBrowserContextAndPage(t *testing.T, options playwright.BrowserNewContextOptions) (playwright.BrowserContext, playwright.Page) {
|
||||||
|
t.Helper()
|
||||||
|
context, err := browser.NewContext(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not create new context: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := context.Close(); err != nil {
|
||||||
|
t.Errorf("could not close context: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
p, err := context.NewPage()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not create new page: %v", err)
|
||||||
|
}
|
||||||
|
return context, p
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFullPath(relativePath string) string {
|
||||||
|
return baseUrL.ResolveReference(&url.URL{Path: relativePath}).String()
|
||||||
|
}
|
16
e2e/home_test.go
Normal file
16
e2e/home_test.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//go:build e2e
|
||||||
|
|
||||||
|
package e2e_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHome(t *testing.T) {
|
||||||
|
beforeEach(t)
|
||||||
|
_, err := page.Goto(getFullPath(""))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, expect.Locator(page.GetByText("Welcome!")).ToBeVisible())
|
||||||
|
}
|
1
e2e/testdata/seed.sql
vendored
Normal file
1
e2e/testdata/seed.sql
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-- TODO: fill this what you need for E2E testing
|
43
go.mod
Normal file
43
go.mod
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
module vctp
|
||||||
|
|
||||||
|
go 1.23.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/a-h/templ v0.2.771
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.17.1
|
||||||
|
github.com/playwright-community/playwright-go v0.4201.1
|
||||||
|
github.com/stretchr/testify v1.8.4
|
||||||
|
github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535
|
||||||
|
modernc.org/sqlite v1.32.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||||
|
github.com/coder/websocket v1.8.12 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
|
||||||
|
github.com/go-stack/stack v1.8.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
|
||||||
|
golang.org/x/sys v0.24.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
|
||||||
|
modernc.org/libc v1.59.9 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.8.0 // indirect
|
||||||
|
modernc.org/strutil v1.2.0 // indirect
|
||||||
|
modernc.org/token v1.1.0 // indirect
|
||||||
|
)
|
116
go.sum
Normal file
116
go.sum
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
github.com/a-h/templ v0.2.771 h1:4KH5ykNigYGGpCe0fRJ7/hzwz72k3qFqIiiLLJskbSo=
|
||||||
|
github.com/a-h/templ v0.2.771/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||||
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
|
||||||
|
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
||||||
|
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
||||||
|
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||||
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||||
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/playwright-community/playwright-go v0.4201.1 h1:fFX/02r3wrL+8NB132RcduR0lWEofxRDJEKuln+9uMQ=
|
||||||
|
github.com/playwright-community/playwright-go v0.4201.1/go.mod h1:hpEOnUo/Kgb2lv5lEY29jbW5Xgn7HaBeiE+PowRad8k=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535 h1:iLjJLq2A5J6L9zrhyNn+fpmxFvtEpYB4XLMr0rX3epI=
|
||||||
|
github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
|
||||||
|
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||||
|
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||||
|
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||||
|
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||||
|
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||||
|
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||||
|
modernc.org/ccgo/v4 v4.20.7 h1:skrinQsjxWfvj6nbC3ztZPJy+NuwmB3hV9zX/pthNYQ=
|
||||||
|
modernc.org/ccgo/v4 v4.20.7/go.mod h1:UOkI3JSG2zT4E2ioHlncSOZsXbuDCZLvPi3uMlZT5GY=
|
||||||
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
|
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
|
||||||
|
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M=
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||||
|
modernc.org/libc v1.59.9 h1:k+nNDDakwipimgmJ1D9H466LhFeSkaPPycAs1OZiDmY=
|
||||||
|
modernc.org/libc v1.59.9/go.mod h1:EY/egGEU7Ju66eU6SBqCNYaFUDuc4npICkMWnU5EE3A=
|
||||||
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
|
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||||
|
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||||
|
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
|
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||||
|
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||||
|
modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s=
|
||||||
|
modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
|
||||||
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
100
log/log.go
Normal file
100
log/log.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New creates a new logger with the given level and output.
|
||||||
|
func New(level Level, output Output) *slog.Logger {
|
||||||
|
var h slog.Handler
|
||||||
|
switch output {
|
||||||
|
case OutputJson:
|
||||||
|
h = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level.ToSlog()})
|
||||||
|
case OutputText:
|
||||||
|
h = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level.ToSlog()})
|
||||||
|
default:
|
||||||
|
h = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level.ToSlog()})
|
||||||
|
}
|
||||||
|
return slog.New(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level represents the log level.
|
||||||
|
type Level string
|
||||||
|
|
||||||
|
// ToSlog converts the level to slog.Level.
|
||||||
|
func (l Level) ToSlog() slog.Level {
|
||||||
|
switch l {
|
||||||
|
case LevelDebug:
|
||||||
|
return slog.LevelDebug
|
||||||
|
case LevelInfo:
|
||||||
|
return slog.LevelInfo
|
||||||
|
case LevelWarn:
|
||||||
|
return slog.LevelWarn
|
||||||
|
case LevelError:
|
||||||
|
return slog.LevelError
|
||||||
|
default:
|
||||||
|
return slog.LevelInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// LogDebug is the debug log level.
|
||||||
|
LevelDebug Level = "debug"
|
||||||
|
// LogInfo is the info log level.
|
||||||
|
LevelInfo Level = "info"
|
||||||
|
// LogWarn is the warn log level.
|
||||||
|
LevelWarn Level = "warn"
|
||||||
|
// LogError is the error log level.
|
||||||
|
LevelError Level = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToLevel converts the level to Level.
|
||||||
|
func ToLevel(level string) Level {
|
||||||
|
switch level {
|
||||||
|
case "debug":
|
||||||
|
return LevelDebug
|
||||||
|
case "info":
|
||||||
|
return LevelInfo
|
||||||
|
case "warn":
|
||||||
|
return LevelWarn
|
||||||
|
case "error":
|
||||||
|
return LevelError
|
||||||
|
default:
|
||||||
|
return LevelInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLevel returns the log level from the environment variable.
|
||||||
|
func GetLevel() Level {
|
||||||
|
level := os.Getenv("LOG_LEVEL")
|
||||||
|
return ToLevel(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output represents the log output.
|
||||||
|
type Output string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// OutputJson is the JSON log output.
|
||||||
|
OutputJson Output = "json"
|
||||||
|
// OutputText is the text log output.
|
||||||
|
OutputText Output = "text"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToOutput converts the output to Output.
|
||||||
|
func ToOutput(output string) Output {
|
||||||
|
switch output {
|
||||||
|
case "json":
|
||||||
|
return OutputJson
|
||||||
|
case "text":
|
||||||
|
return OutputText
|
||||||
|
default:
|
||||||
|
return OutputText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOutput returns the log output from the environment variable.
|
||||||
|
func GetOutput() Output {
|
||||||
|
output := os.Getenv("LOG_OUTPUT")
|
||||||
|
return ToOutput(output)
|
||||||
|
}
|
44
main.go
Normal file
44
main.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"vctp/db"
|
||||||
|
"vctp/log"
|
||||||
|
"vctp/server"
|
||||||
|
"vctp/server/router"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger := log.New(
|
||||||
|
log.GetLevel(),
|
||||||
|
log.GetOutput(),
|
||||||
|
)
|
||||||
|
|
||||||
|
database, err := db.New(logger, "./db.sqlite3")
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to create database", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer database.Close()
|
||||||
|
|
||||||
|
if err = db.Migrate(database); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||||
|
logger.Error("failed to migrate database", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
svr := server.New(
|
||||||
|
logger,
|
||||||
|
":"+port,
|
||||||
|
server.WithRouter(router.New(logger, database)),
|
||||||
|
)
|
||||||
|
|
||||||
|
svr.StartAndWait()
|
||||||
|
}
|
24
server/handler/handler.go
Normal file
24
server/handler/handler.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
"vctp/db"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler handles requests.
|
||||||
|
type Handler struct {
|
||||||
|
Logger *slog.Logger
|
||||||
|
Database db.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) html(ctx context.Context, w http.ResponseWriter, status int, t templ.Component) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
|
||||||
|
if err := t.Render(ctx, w); err != nil {
|
||||||
|
h.Logger.Error("Failed to render component", "error", err)
|
||||||
|
}
|
||||||
|
}
|
12
server/handler/home.go
Normal file
12
server/handler/home.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"vctp/components/core"
|
||||||
|
"vctp/components/home"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Home handles the home page.
|
||||||
|
func (h *Handler) Home(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.html(r.Context(), w, http.StatusOK, core.HTML("Example Site", home.Home()))
|
||||||
|
}
|
18
server/middleware/cache.go
Normal file
18
server/middleware/cache.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"vctp/version"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CacheMiddleware sets the Cache-Control header based on the version.
|
||||||
|
func CacheMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if version.Value == "dev" {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
34
server/middleware/logging.go
Normal file
34
server/middleware/logging.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoggingMiddleware represents a logging middleware.
|
||||||
|
type LoggingMiddleware struct {
|
||||||
|
logger *slog.Logger
|
||||||
|
handler http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLoggingMiddleware creates a new logging middleware with the given logger and handler.
|
||||||
|
func NewLoggingMiddleware(logger *slog.Logger, handler http.Handler) *LoggingMiddleware {
|
||||||
|
return &LoggingMiddleware{
|
||||||
|
logger: logger,
|
||||||
|
handler: handler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP logs the request and calls the next handler.
|
||||||
|
func (l *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
l.handler.ServeHTTP(w, r)
|
||||||
|
l.logger.Debug(
|
||||||
|
"Request recieved",
|
||||||
|
slog.String("method", r.Method),
|
||||||
|
slog.String("path", r.URL.Path),
|
||||||
|
slog.String("remote", r.RemoteAddr),
|
||||||
|
slog.Duration("duration", time.Since(start)),
|
||||||
|
)
|
||||||
|
}
|
22
server/middleware/middleware.go
Normal file
22
server/middleware/middleware.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type Handler func(http.Handler) http.Handler
|
||||||
|
|
||||||
|
func Chain(handlers ...Handler) Handler {
|
||||||
|
if len(handlers) == 0 {
|
||||||
|
return defaultHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
for i := len(handlers) - 1; i >= 0; i-- {
|
||||||
|
next = handlers[i](next)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultHandler(next http.Handler) http.Handler {
|
||||||
|
return next
|
||||||
|
}
|
24
server/router/router.go
Normal file
24
server/router/router.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"vctp/db"
|
||||||
|
"vctp/dist"
|
||||||
|
"vctp/server/handler"
|
||||||
|
"vctp/server/middleware"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(logger *slog.Logger, database db.Database) http.Handler {
|
||||||
|
h := &handler.Handler{
|
||||||
|
Logger: logger,
|
||||||
|
Database: database,
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
mux.Handle("/assets/", middleware.CacheMiddleware(http.FileServer(http.FS(dist.AssetsDir))))
|
||||||
|
mux.HandleFunc("/", h.Home)
|
||||||
|
|
||||||
|
return middleware.NewLoggingMiddleware(logger, mux)
|
||||||
|
}
|
96
server/server.go
Normal file
96
server/server.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server represents an HTTP server.
|
||||||
|
type Server struct {
|
||||||
|
srv *http.Server
|
||||||
|
logger *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new server with the given logger, address and options.
|
||||||
|
func New(logger *slog.Logger, addr string, opts ...Option) *Server {
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
WriteTimeout: 15 * time.Second,
|
||||||
|
ReadTimeout: 15 * time.Second,
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&Server{srv: srv})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Server{
|
||||||
|
srv: srv,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option represents a server option.
|
||||||
|
type Option func(*Server)
|
||||||
|
|
||||||
|
// WithWriteTimeout sets the write timeout.
|
||||||
|
func WithWriteTimeout(timeout time.Duration) Option {
|
||||||
|
return func(s *Server) {
|
||||||
|
s.srv.WriteTimeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithReadTimeout sets the read timeout.
|
||||||
|
func WithReadTimeout(timeout time.Duration) Option {
|
||||||
|
return func(s *Server) {
|
||||||
|
s.srv.ReadTimeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRouter sets the handler.
|
||||||
|
func WithRouter(handler http.Handler) Option {
|
||||||
|
return func(s *Server) {
|
||||||
|
s.srv.Handler = handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartAndWait starts the server and waits for a signal to shut down.
|
||||||
|
func (s *Server) StartAndWait() {
|
||||||
|
s.Start()
|
||||||
|
s.GracefulShutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the server.
|
||||||
|
func (s *Server) Start() {
|
||||||
|
go func() {
|
||||||
|
s.logger.Info("starting server", "port", s.srv.Addr)
|
||||||
|
if err := s.srv.ListenAndServe(); err != nil {
|
||||||
|
s.logger.Warn("failed to start server", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GracefulShutdown shuts down the server gracefully.
|
||||||
|
func (s *Server) GracefulShutdown() {
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
||||||
|
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
|
||||||
|
signal.Notify(c, os.Interrupt)
|
||||||
|
|
||||||
|
// Block until we receive our signal.
|
||||||
|
<-c
|
||||||
|
|
||||||
|
// Create a deadline to wait for.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
// Doesn't block if no connections, but will otherwise wait
|
||||||
|
// until the timeout deadline.
|
||||||
|
_ = s.srv.Shutdown(ctx)
|
||||||
|
// Optionally, you could run srv.Shutdown in a goroutine and block on
|
||||||
|
// <-ctx.Done() if your application should wait for other services
|
||||||
|
// to finalize based on context cancellation.
|
||||||
|
s.logger.Info("shutting down")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
10
sqlc.yml
Normal file
10
sqlc.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
version: 2
|
||||||
|
sql:
|
||||||
|
- engine: sqlite
|
||||||
|
queries:
|
||||||
|
- db/queries/query.sql
|
||||||
|
schema: db/migrations
|
||||||
|
gen:
|
||||||
|
go:
|
||||||
|
package: queries
|
||||||
|
out: db/queries
|
3
styles/input.css
Normal file
3
styles/input.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
14
tailwind.config.js
Normal file
14
tailwind.config.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./components/**/*.templ",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
require('@tailwindcss/forms'),
|
||||||
|
require('@tailwindcss/typography'),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
10
update_module.sh
Executable file
10
update_module.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ "$#" -ne 1 ]; then
|
||||||
|
echo "Usage: $0 <new module name>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
new_module=$1
|
||||||
|
|
||||||
|
find . -type d -name .git -prune -o -type f -exec sed -i '' -e "s/vctp/${new_module}/g" {} \;
|
22
upgrade_htmx.sh
Executable file
22
upgrade_htmx.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ "$#" -ne 1 ]; then
|
||||||
|
echo "Usage: $0 <new htmx version>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
old_version=""
|
||||||
|
new_version=$1
|
||||||
|
|
||||||
|
for filename in "./dist/assets/js"/*; do
|
||||||
|
if [[ "$filename" == "./dist/assets/js/htmx"* ]]; then
|
||||||
|
old_version=$(echo "$filename" | awk -F'@' '{gsub(/\.min\.js/, "", $2); print $2}')
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
curl -sL -o "./dist/assets/js/htmx@${new_version}.min.js" "https://github.com/bigskysoftware/htmx/releases/download/${new_version}/htmx.min.js"
|
||||||
|
|
||||||
|
sed -i '' -e "s/${old_version}/${new_version}/g" "./components/core/html.templ"
|
||||||
|
|
||||||
|
rm "./dist/assets/js/htmx@${old_version}.min.js"
|
13
upgrade_sqlc.sh
Executable file
13
upgrade_sqlc.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
url=$(curl -s 'https://downloads.sqlc.dev/')
|
||||||
|
|
||||||
|
new_version=$(echo "$url" | sed -e 's/<li>/<li>\n/g' | tr -d '\n' | sed -e 's/<li>/\n<li>/g' | sed -e 's/<\/li>/<\/li>\n/g' | grep -o '<li>[^<]*<a[^>]*>[^<]*</a>[^<]*</li>' | sed -e 's/<[^>]*>//g' | tail -n 1 | awk '{$1=$1;print}')
|
||||||
|
|
||||||
|
old_version=$(grep 'sqlc-version' .github/workflows/ci.yml | awk '{print $2}' | tr -d "'" | head -n 1)
|
||||||
|
|
||||||
|
for file in ".github/workflows"/*; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
sed -i '' -e "s/${old_version}/${new_version}/g" "$file"
|
||||||
|
fi
|
||||||
|
done
|
18
upgrade_templ.sh
Executable file
18
upgrade_templ.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
old_version=$(grep 'github.com/a-h/templ' go.mod | awk '{print $2}')
|
||||||
|
|
||||||
|
go install github.com/a-h/templ/cmd/templ@latest
|
||||||
|
|
||||||
|
go get -u github.com/a-h/templ
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
new_version=$(grep 'github.com/a-h/templ' go.mod | awk '{print $2}')
|
||||||
|
|
||||||
|
sed -i '' -e "s/${old_version}/${new_version}/g" "Dockerfile"
|
||||||
|
|
||||||
|
for file in ".github/workflows"/*; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
sed -i '' -e "s/${old_version}/${new_version}/g" "$file"
|
||||||
|
fi
|
||||||
|
done
|
4
version/version.go
Normal file
4
version/version.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
// Value is the version. This is set at build time.
|
||||||
|
var Value = "dev"
|
Reference in New Issue
Block a user