This file contains a list of tasks that are either required or at least strongly recommended to align projects using this SDK.
Our previous approach to have a single Server
struct and to put all logic there gets messy pretty fast. Worse than
that it is hard to refactor this to separate this into multiple structs or packages. Dependency injection will help us
to separate those things in the very beginning of a project without manual wiring of dependencies.
- The primary server.Run function should be replaced with a standalone
RunServer(ctx context.Context, c *dig.Container) error
- The
cmdutil.Runner
should setup environment specific dependencies (eg Redis vs Miniredis) whileRunServer
should setup the independent ones (eg services, workers and handlers).
The RunServer
function could look like this:
func RunServer(ctx context.Context, c *dig.Container) error {
return errors.Join(
// define services and repos
c.Provide(...),
// define HTTP handlers
webutil.ProvideHandler(c, handlers.New...),
// define workers
runutil.ProvideWorker(c, workers.New...),
// start all workers
runutil.RunProvidedWorkers(ctx, c),
)
}
The setup of the HTTP server has a lot of repetition in every project. Therefore, we move it into the SDK.
Defining the fs.FS
types for assets and templates for prod and dev is always a bit cumbersome, because the paths for
devs are always relative to the project root and the paths for the prod ones need to do an additional fs.Sub
with
respective error handling. This gets a little more severe with dependency injection, since we would have to wrap this into functions.
The package structure also is not optimal, since the templates are currently nested into the handlers package, which is one level below all other directories. Generally, the package structure is not defined properly yet.
To make our lives easier with dependency injection, we should also define distinct types for the asset fs.FS
and the
template fs.FS
.
pkg/app/handlers
— Should include all HTTP handlers.pkg/app/templates
— Should include all HTML templates.web
— Should include the frontend assets. Mostly generated by Yarn.
The file is more or less static and would only differ in the generate statements, if you are not using Yarn.
package web
import (
"embed"
"io/fs"
"os"
)
//go:generate yarn install
//go:generate yarn build
//go:embed all:dist/*
var embedded embed.FS
type FS fs.FS
func DevFS() FS {
return os.DirFS("web/dist")
}
func ProdFS() FS {
result, err := fs.Sub(embedded, "dist")
if err != nil {
panic(err)
}
return result
}
This file should look the same everywhere and should be the only .go
file in that directory, because the other ones
are HTML templates.
package templates
import (
"embed"
"io/fs"
"os"
)
//go:embed all:*
var embedded embed.FS
type FS fs.FS
func DevFS() FS {
return os.DirFS("pkg/app/templates")
}
func ProdFS() FS {
return embedded
}
When already using dependency injection, the definitions in the root.go
would look simple as follows, for production
and development respectively:
c.Provide(templates.ProdFS)
c.Provide(web.ProdFS)
c.Provide(templates.DevFS)
c.Provide(web.DevFS)
Afterward, they can be simply requested by requesting web.FS
or templates.FS
.
Our previous approach to have a single Server
struct and to put all logic there gets messy pretty fast. Worse than
that it is hard to refactor this to separate this into multiple structs or packages. Dependency injection will help us
to separate those things in the very beginning of a project without manual wiring of dependencies. One step towards this
to split up all HTTP handlers into separate files.
- The handlers should be moved into the
pkg/app/handlers
package. - The handler struct should have a
New...
constructor with all required dependencies as parameters. - The handler struct should implement
interface { Register(chi.Router) }
, which gets called once to set up the routes. This will later be used for dependency injection integration.
package handlers
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/rebuy-de/platform-inventory/pkg/bll/webutilext"
"github.com/rebuy-de/platform-inventory/pkg/dal/sqlc"
"github.com/rebuy-de/rebuy-go-sdk/v8/pkg/webutil"
)
type KubeEventHandler struct {
sqlc *sqlc.Queries
viewer *webutilext.JetViewer
}
func NewKubeEventHandler(
sqlc *sqlc.Queries,
viewer *webutilext.JetViewer,
) *KubeEventHandler {
return &KubeEventHandler{
sqlc: sqlc,
viewer: viewer,
}
}
func (h *KubeEventHandler) Register(router chi.Router) {
router.Get("/kube/events", webutilext.WrapView(h.list))
router.Get("/kube/events/table-fragment", webutilext.WrapView(h.listFragment))
}
func (h *KubeEventHandler) list(r *http.Request) webutil.Response {
events, err := h.sqlc.ListKubeEvents(r.Context())
if err != nil {
return webutilext.ViewError(http.StatusInternalServerError, err)
}
return h.viewer.HTML(http.StatusOK, "kube_event_list.html", events)
}
func (h *KubeEventHandler) listFragment(r *http.Request) webutil.Response {
events, err := h.sqlc.ListKubeEvents(r.Context())
if err != nil {
return webutilext.ViewError(http.StatusInternalServerError, err)
}
return h.viewer.HTML(http.StatusOK, "frames/kube_event_table.html", events)
}
The previous interface is a bit awkward to use with dependency injection together with splitting the HTTP handlers into
multiple structs. Additionally there were cases where we wanted to use the webutil.Response
type for convenience, but
did not actually need any HTML rendering.
Therefore the interfaces are changed this way:
- The new
webuitil.WrapView
function replaces the oldwebuitl.ViewHandler.Wrap
function and does not require any template definitions. - All
webutil.Response
functions, that do not need templates, are pure functions now (ie not attached to a type). - The handler interface gets reduced to
func(*http.Request) Response
, so it does not contain the view parameter anymore. When using HTML, it is required to attach thewebutil.GoTemplateViewer
to the struct that implements the handler.
Our cdnmirror
is quite limited and we have no means of running Dependabot on those dependencies. Also with Yarn we are able to minify our own JS and CSS files.
- Move static assets to
web/src/www
.- eg
mkdir -p web/src && mv cmd/assets web/src/www && rm -rf web/src/www/cdnmirror
- eg
- Custom style should go into
web/src/index.css
.- The file can be a composition of
@import "xxx";
statements.
- The file can be a composition of
- Custom style should go into
web/src/index.js
.- The file can be a composition of
import 'xxx';
statements.
- The file can be a composition of
web/web.go
package web
import (
"embed"
"io/fs"
"os"
"github.com/rebuy-de/rebuy-go-sdk/v8/pkg/webutil"
)
//go:generate yarn install
//go:generate yarn build
//go:embed all:dist/*
var embedded embed.FS
func DevFS() webutil.AssetFS {
return os.DirFS("web/dist")
}
func ProdFS() webutil.AssetFS {
result, err := fs.Sub(embedded, "dist")
if err != nil {
panic(err)
}
return result
}
web/package.json
- Set project name (kebab case).
- Adjust dependencies.
{
"name": "__PROJECT_NAME__",
"version": "1.0.0",
"packageManager": "[email protected]",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.2",
"bulma": "^1.0.1",
"htmx.org": "^1.9.12",
"idiomorph": "^0.3.0"
},
"devDependencies": {
"esbuild": "^0.23.0",
"nodemon": "^3.1.4"
},
"scripts": {
"build": "node esbuild.config.mjs"
}
}
web/esbuild.config.mjs
- Remove files from
entryPoints
if not needed. Remove the wholeesbuild.build
command, if theentryPoints
are empty. - Remove
fs.cpSync
, if there are not static assets, like favicons.
import * as esbuild from 'esbuild'
import fs from 'node:fs'
await esbuild.build({
entryPoints: [
'src/index.js', 'src/index.css',
],
bundle: true,
minify: true,
sourcemap: true,
outdir: 'dist/',
format: 'esm',
loader: {
'.woff2': 'file',
'.ttf': 'file'
},
})
fs.cpSync('src/www', 'dist', {recursive: true});
// The HTMX stuff does not deal well with ESM bundling. It is not needed tho,
// therefore we copy the assets manually and link them directly in the <head>.
const scripts = [
'hyperscript.org/dist/_hyperscript.min.js',
'hyperscript.org/dist/template.js',
'htmx.org/dist/htmx.min.js',
'idiomorph/dist/idiomorph-ext.min.js',
];
scripts.forEach((file) => {
fs.cpSync(`node_modules/${file}`, `dist/${file}`, {recursive: true});
});
*web/.gitattributes`
.yarn/releases/* filter=lfs diff=lfs merge=lfs -text
web/.gitignore
/node_modules
/dist/*
!/dist/.keepdir
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
web/.yarnrc.yml
yarnPath: .yarn/releases/yarn-4.2.2.cjs
enableGlobalCache: false
nmMode: hardlinks-global
nodeLinker: node-modules
There might be a better way.
mkdir -p web/.yarn/releases/
curl -sSL -o web/.yarn/releases/yarn-4.2.2.cjs https://repo.yarnpkg.com/4.2.2/packages/yarnpkg-cli/bin/yarn.js
go generate ./web
This should run without errors and generate the directories web/dist
and web/node_modules
.
- remove from
tools.go
- remove from all
go:generate
comments - run
go mod tidy
- everything in
src/index.js
is merged and minified and available with/assets/index.js
- everything in
src/index.css
is merged and minified and available with/assets/index.css
- use
web.ProdFS()
instead of//go:embed
- use
web.DevFS()
instead ofos.DirFS("cmd/assets")
in dev command
Configure .github/dependabot.yml
:
- package-ecosystem: "npm"
directory: "/web"
schedule:
interval: "weekly"
day: "tuesday"
time: "10:00"
timezone: "Europe/Berlin"
groups:
yarn:
patterns:
- "*"
With Alpine it would look like this:
RUN apk add --no-cache git git-lfs openssl nodejs yarn
@import "@fortawesome/fontawesome-free/css/all.css";
@import "bulma/css/bulma.css";
@import './style/bulma-patch.css';
@import './style/ry.css';
Most config can be infered from the go:generate configs. For example with this unkpg source:
//go:generate cdnmirror --source https://unpkg.com/[email protected]/dist/css/bootstrap.min.css --target bootstrap-5.1.3-min.css
The URL follows this pattern: https://unpkg.com/{package}@{version}/{import}
, where:
{package}
and{version}
should go into thedependencies
config ofpackage.json
.- For CSS, add a statement like this into the
index.css
:@import "{package}/{import}";
There is not guarantee that this work, tho.
github.com/pkg/errorsis deprecated. Since the built-in
errorspackage improved a bit in the recent Go versions, we should remove all uses of
github.com/pkg/errorsand replace it with the
errors` package.
- Use the pattern
return fmt.Errorf("something happenend with %#v: %w", someID, err)
- The stack trace feature gets lost. Therefore it is suggested to properly add error messages each time handling errors.