feat: modernise template with core/go-api provider pattern
Replace Cobra CLI + raw net/http with core/go-api Engine. DemoProvider implements RouteGroup for plug-and-play registration. Lit element updated to fetch from Go API. Add .core/build.yaml and CLAUDE.md. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
a5de52ac6e
commit
5143c211d3
13 changed files with 295 additions and 123 deletions
7
.core/build.yaml
Normal file
7
.core/build.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
project: element-template
|
||||
binary: core-element-template
|
||||
targets:
|
||||
- os: darwin
|
||||
arch: arm64
|
||||
- os: linux
|
||||
arch: amd64
|
||||
43
CLAUDE.md
Normal file
43
CLAUDE.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code when working with this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Starter template for building custom HTML elements backed by a Go API using the Core ecosystem. Clone this to create a new service provider that can plug into core/ide.
|
||||
|
||||
## Build & Development
|
||||
|
||||
```bash
|
||||
# Build UI (Lit custom element)
|
||||
cd ui && npm install && npm run build
|
||||
|
||||
# Run development server
|
||||
go run . --port 8080
|
||||
|
||||
# Build binary
|
||||
core build
|
||||
|
||||
# Quality assurance
|
||||
core go qa
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
- **`main.go`** — Creates `api.Engine`, registers providers, serves embedded UI
|
||||
- **`provider.go`** — Example `DemoProvider` implementing `api.RouteGroup`
|
||||
- **`static.go`** — Static file serving helper
|
||||
- **`ui/`** — Lit custom element that talks to the Go API via fetch
|
||||
|
||||
## Creating Your Own Provider
|
||||
|
||||
1. Rename `DemoProvider` in `provider.go`
|
||||
2. Update `Name()`, `BasePath()`, and routes in `RegisterRoutes()`
|
||||
3. Update the Lit element in `ui/src/index.ts` to match your API
|
||||
4. Update the custom element tag in `ui/index.html`
|
||||
|
||||
## Conventions
|
||||
|
||||
- UK English in all user-facing strings
|
||||
- EUPL-1.2 licence
|
||||
- Conventional commits
|
||||
73
README.md
73
README.md
|
|
@ -1,43 +1,52 @@
|
|||
# Core Element Template
|
||||
|
||||
This repository is a template for developers to create custom HTML elements for the core web3 framework. It includes a Go backend, a Lit-based custom element, and a full release cycle configuration.
|
||||
Starter template for building custom HTML elements backed by a Go API. Part of the [Core ecosystem](https://core.help).
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/your-username/core-element-template.git
|
||||
```
|
||||
|
||||
2. **Install the dependencies:**
|
||||
```bash
|
||||
cd core-element-template
|
||||
go mod tidy
|
||||
cd ui
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Run the development server:**
|
||||
```bash
|
||||
go run ./cmd/demo-cli serve
|
||||
```
|
||||
This will start the Go backend and serve the Lit custom element.
|
||||
|
||||
## Building the Custom Element
|
||||
|
||||
To build the Lit custom element, run the following command:
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
npm run build
|
||||
# Clone and rename
|
||||
git clone https://forge.lthn.ai/core/element-template.git my-element
|
||||
cd my-element
|
||||
|
||||
# Install UI dependencies and build
|
||||
cd ui && npm install && npm run build && cd ..
|
||||
|
||||
# Run
|
||||
go run . --port 8080
|
||||
```
|
||||
|
||||
This will create a JavaScript file in the `dist` directory that you can use in any HTML page.
|
||||
Open `http://localhost:8080` — you'll see the `<core-demo-element>` fetching data from the Go API.
|
||||
|
||||
## Contributing
|
||||
## What's Included
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
| Component | Technology | Purpose |
|
||||
|-----------|-----------|---------|
|
||||
| Go backend | core/go-api (Gin) | REST API with CORS, middleware |
|
||||
| Custom element | Lit 3 | Self-contained web component |
|
||||
| Build config | `.core/build.yaml` | Cross-platform binary builds |
|
||||
|
||||
## License
|
||||
## Making It Yours
|
||||
|
||||
This project is licensed under the EUPL-1.2 License - see the [LICENSE](LICENSE) file for details.
|
||||
1. Update `go.mod` module path
|
||||
2. Rename `DemoProvider` in `provider.go` — implement your API
|
||||
3. Rename `CoreDemoElement` in `ui/src/index.ts` — implement your UI
|
||||
4. Update the element tag in `ui/index.html`
|
||||
|
||||
## Service Provider Pattern
|
||||
|
||||
The `DemoProvider` implements `api.RouteGroup`:
|
||||
|
||||
```go
|
||||
func (p *DemoProvider) Name() string { return "demo" }
|
||||
func (p *DemoProvider) BasePath() string { return "/api/v1/demo" }
|
||||
func (p *DemoProvider) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
rg.GET("/hello", p.hello)
|
||||
}
|
||||
```
|
||||
|
||||
Register it with `engine.Register(&DemoProvider{})` and it gets middleware, CORS, and OpenAPI for free. The same provider can plug into core/ide's registry.
|
||||
|
||||
## Licence
|
||||
|
||||
EUPL-1.2
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "demo-cli",
|
||||
Short: "A demo CLI for the core-element-template",
|
||||
Long: `A longer description that spans multiple lines and likely contains examples and usage of using your application.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Hello from the demo CLI!")
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Starts the HTTP server",
|
||||
Long: `Starts the HTTP server to serve the frontend and the API.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
http.HandleFunc("/api/v1/demo", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Hello, world!")
|
||||
})
|
||||
|
||||
fs := http.FileServer(http.Dir("./ui/dist"))
|
||||
http.Handle("/", fs)
|
||||
|
||||
log.Println("Listening on :8080...")
|
||||
err := http.ListenAndServe(":8080", nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/your-username/core-element-template/cmd/demo-cli/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
10
go.mod
10
go.mod
|
|
@ -1,9 +1,9 @@
|
|||
module github.com/your-username/core-element-template
|
||||
module forge.lthn.ai/core/element-template
|
||||
|
||||
go 1.24.3
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/cobra v1.10.1 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
forge.lthn.ai/core/go-api v0.1.0
|
||||
forge.lthn.ai/core/go-log v0.0.1
|
||||
forge.lthn.ai/core/go-ws v0.1.3
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
2025/11/14 15:44:19 Listening on :8080...
|
||||
2025/11/14 15:44:19 listen tcp :8080: bind: address already in use
|
||||
exit status 1
|
||||
72
main.go
Normal file
72
main.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
// core-element-template demonstrates the service provider pattern.
|
||||
// It serves a Lit custom element backed by a Go API using core/go-api.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run . [--port 8080]
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"flag"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
api "forge.lthn.ai/core/go-api"
|
||||
"forge.lthn.ai/core/go-log"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
//go:embed all:ui/dist
|
||||
var uiAssets embed.FS
|
||||
|
||||
func main() {
|
||||
port := flag.String("port", "8080", "HTTP server port")
|
||||
flag.Parse()
|
||||
|
||||
logger := log.New("element-template")
|
||||
|
||||
// Create API engine with middleware
|
||||
engine, err := api.New(
|
||||
api.WithCORS(),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error("failed to create API engine", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Register the demo provider
|
||||
engine.Register(&DemoProvider{})
|
||||
|
||||
// Serve the Lit custom element UI as static files
|
||||
staticFS, err := fs.Sub(uiAssets, "ui/dist")
|
||||
if err != nil {
|
||||
logger.Error("failed to load UI assets", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
engine.Router().NoRoute(gin.WrapH(
|
||||
noCache(staticFS),
|
||||
))
|
||||
|
||||
// Start server
|
||||
ctx, cancel := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
addr := ":" + *port
|
||||
logger.Info("starting server", "addr", addr)
|
||||
|
||||
go func() {
|
||||
if err := engine.Serve(ctx, addr); err != nil {
|
||||
logger.Error("server error", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
logger.Info("shutting down")
|
||||
}
|
||||
42
provider.go
Normal file
42
provider.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DemoProvider is an example service provider that exposes a REST API.
|
||||
// Replace this with your own provider implementation.
|
||||
type DemoProvider struct{}
|
||||
|
||||
// Name implements api.RouteGroup.
|
||||
func (p *DemoProvider) Name() string { return "demo" }
|
||||
|
||||
// BasePath implements api.RouteGroup.
|
||||
func (p *DemoProvider) BasePath() string { return "/api/v1/demo" }
|
||||
|
||||
// RegisterRoutes implements api.RouteGroup.
|
||||
func (p *DemoProvider) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
rg.GET("/hello", p.hello)
|
||||
rg.GET("/status", p.status)
|
||||
}
|
||||
|
||||
func (p *DemoProvider) hello(c *gin.Context) {
|
||||
name := c.DefaultQuery("name", "World")
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Hello, " + name + "!",
|
||||
})
|
||||
}
|
||||
|
||||
func (p *DemoProvider) status(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "running",
|
||||
"uptime": time.Since(startTime).String(),
|
||||
})
|
||||
}
|
||||
|
||||
var startTime = time.Now()
|
||||
17
static.go
Normal file
17
static.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// noCache wraps a filesystem with cache-busting headers for development.
|
||||
func noCache(fsys fs.FS) http.Handler {
|
||||
fileServer := http.FileServer(http.FS(fsys))
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
fileServer.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hello World</title>
|
||||
<title>Core Element Demo</title>
|
||||
<script type="module" src="/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<hello-world-element name="Your Name"></hello-world-element>
|
||||
<core-demo-element></core-demo-element>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,30 +1,81 @@
|
|||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
/**
|
||||
* A simple "Hello World" custom element.
|
||||
* A demo custom element backed by a Go API.
|
||||
* Replace this with your own element implementation.
|
||||
*
|
||||
* @element hello-world-element
|
||||
* @element core-demo-element
|
||||
*/
|
||||
@customElement('hello-world-element')
|
||||
export class HelloWorldElement extends LitElement {
|
||||
/**
|
||||
* The name to say hello to.
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String })
|
||||
name = 'World';
|
||||
@customElement('core-demo-element')
|
||||
export class CoreDemoElement extends LitElement {
|
||||
/** The API base URL. Defaults to current origin. */
|
||||
@property({ type: String, attribute: 'api-url' })
|
||||
apiUrl = '';
|
||||
|
||||
@state()
|
||||
private message = 'Loading...';
|
||||
|
||||
@state()
|
||||
private uptime = '';
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
border: solid 1px gray;
|
||||
padding: 16px;
|
||||
max-width: 800px;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.5rem;
|
||||
max-width: 480px;
|
||||
}
|
||||
h2 {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1.25rem;
|
||||
color: #1a1b26;
|
||||
}
|
||||
.status {
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
`;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
private async fetchData() {
|
||||
const base = this.apiUrl || window.location.origin;
|
||||
try {
|
||||
const [helloRes, statusRes] = await Promise.all([
|
||||
fetch(`${base}/api/v1/demo/hello`),
|
||||
fetch(`${base}/api/v1/demo/status`),
|
||||
]);
|
||||
const hello = await helloRes.json();
|
||||
const status = await statusRes.json();
|
||||
this.message = hello.message;
|
||||
this.uptime = status.uptime;
|
||||
} catch {
|
||||
this.message = 'Failed to connect to API';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<h1>Hello, ${this.name}!</h1>`;
|
||||
return html`
|
||||
<h2>${this.message}</h2>
|
||||
<div class="status">
|
||||
<span class="badge">running</span>
|
||||
Uptime: ${this.uptime}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue