I want to change a page without first getting a generated client back into agreement with the Go behind it, because the connection between Go and TypeScript has been taking far too much of my time.
I'm calling this project GoSSR. The idea is server-side rendering with Go producing the HTML from its own values. The browser receives the page. For that rendering step, I can leave out the generated TypeScript client.
That's the decision I'm starting with. The page should use the types I've already declared, and the Go tools should check how it uses them.
Start with the page
I'm trying to show a list of cities. A little Go produces them, and a little markup puts their names on the page. The data is small:
type City struct { Name string }
type PageData struct { Cities []City }
An ordinary server template can already read those Go values:
{{ range .Cities }}<p>{{ .Name }}</p>{{ end }}
That removes the separate frontend client from rendering this list. Go prepares the data, and the template writes the HTML. I don't need a TypeScript version of City for it.
So I can stop generating the client. That still leaves a handler to open, a response type to follow, another template to read. Where does Cities come from? I'm following references between files to understand where two paragraphs of output got their data, and finding it more irritating than the size of the job deserves. All I'm showing is a pair of city names. A small page is still a file I have to spend time in. I'm fussy about that because these are the tools I'm building for client work, and even the boring pages need to be pleasant to change.
GoSSR's component file keeps the page's Go with its template. It uses a script block marked as Go:
<template>
<p p-for="city in state.Cities" p-text="city.Name"></p>
</template>
<script type="application/x-go">
package main
type City struct { Name string }
type Response struct { Cities []City }
func Render() (Response, error) {
return Response{Cities: []City{{"London"}, {"Paris"}}}, nil
}
</script>
Now I can see who makes Cities and who reads it without changing files. The list comes from Render, and the template takes each city's name. This example uses state for that returned response.
London and Paris become two paragraphs.
A rename still requires changing the declaration and its uses. Keeping them close makes the connection easier to follow. Opening the page should show what it needs.
Shared business rules can remain ordinary Go packages that I call from the page's Go. If a page and an API both need to enforce the same business rule, I'd put that check in a package and call it from both. The page can use that check's result to choose what to display. What belongs in the component is the work particular to displaying this page, alongside the markup using its result. That also keeps me from putting the entire application in a script block, I'd like a useful file to edit and I still want to find the shared code where the other Go code lives.
Putting two languages in one file doesn't make their tools understand each other. An editor needs the Go block's start and end. A build needs to connect state.Cities with the response type. Diagnostics must point to the component, not a mysterious generated file.
Let Go check the connection
I'm aiming to compile the template into Go that refers to real fields. Its loop can read pageData.Cities, then city.Name. The normal Go tools can check those expressions against the types used by the application.
That's where this gets more useful than putting blocks beside each other, because renaming Name to Title while the template still reads Name gives the generated Go an ordinary missing-field error. I can keep one Go definition of the data and check the places which use it.
A compiler to look after
I went looking for less connection work, and now I'm building a template compiler.
With html/template, an unknown struct field fails during execution, and HTML output gets contextual escaping. Go's tools can check the generated field access before the page runs. I also have to escape values correctly in each HTML context.
The compiler has to know whether a value is being written as text, an attribute or something else. Moving a typo to compile time is useful. Accidentally moving HTML injection into the application would be a poor exchange.
The trade feels worthwhile to me.
I can improve this tool once and use it across the pages and client projects. The generated code uses the application's Go types directly, so each page doesn't need its own translation into a client model.
Now there's different work. The compiler needs to understand the template directives, preserve the Go expressions and produce code I can inspect. Errors should point to the component I was editing.
For production the generated Go can go through the normal toolchain. During editing the changing Go should run without rebuilding the whole application. That calls for an interpreter, with the compiled and interpreted paths behaving consistently. It's a much larger job than adding the script tag, and probably the part I'll get most absorbed in. A page to change and now a compiler to improve and an interpreter to explore.... This is how side projects breed.
For the cities page, the arrangement is small enough to follow. Go makes the list. The template reads its fields. The result is HTML. I can work on those first two parts together, using the same data, without a generated client between them.
That's the editing unit I'm after with GoSSR.