Changed: DB Params

This commit is contained in:
2025-03-20 12:35:13 +01:00
parent 8640a12439
commit b71b3d12ca
822 changed files with 134218 additions and 0 deletions

3
templ/storybook/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
storybook-server
*storybook.log

19
templ/storybook/_example/cdk/.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# go.sum should be committed
!go.sum
# CDK asset staging directory
.cdk.staging
cdk.out

View File

@@ -0,0 +1,14 @@
# Welcome to your CDK Go project!
This is a blank project for Go development with CDK.
**NOTICE**: Go support is still in Developer Preview. This implies that APIs may
change while we address early feedback from the community. We would love to hear
about your experience through GitHub issues.
## Useful commands
* `cdk deploy` deploy this stack to your default AWS account/region
* `cdk diff` compare deployed stack with current state
* `cdk synth` emits the synthesized CloudFormation template
* `go test` run unit tests

View File

@@ -0,0 +1,87 @@
package main
import (
"github.com/aws/aws-cdk-go/awscdk/v2"
"github.com/aws/aws-cdk-go/awscdk/v2/awslambda"
"github.com/aws/aws-cdk-go/awscdkapigatewayv2alpha/v2"
awsapigatewayv2 "github.com/aws/aws-cdk-go/awscdkapigatewayv2alpha/v2"
awsapigatewayv2integrations "github.com/aws/aws-cdk-go/awscdkapigatewayv2integrationsalpha/v2"
awslambdago "github.com/aws/aws-cdk-go/awscdklambdagoalpha/v2"
"github.com/aws/constructs-go/constructs/v10"
"github.com/aws/jsii-runtime-go"
)
type CdkStackProps struct {
awscdk.StackProps
}
func NewCdkStack(scope constructs.Construct, id string, props *CdkStackProps) awscdk.Stack {
var sprops awscdk.StackProps
if props != nil {
sprops = props.StackProps
}
stack := awscdk.NewStack(scope, &id, &sprops)
bundlingOptions := &awslambdago.BundlingOptions{
GoBuildFlags: &[]*string{jsii.String(`-ldflags "-s -w"`)},
}
f := awslambdago.NewGoFunction(stack, jsii.String("storybookHandler"), &awslambdago.GoFunctionProps{
Runtime: awslambda.Runtime_GO_1_X(),
Entry: jsii.String("../lambda"),
Bundling: bundlingOptions,
MemorySize: jsii.Number(1024),
Timeout: awscdk.Duration_Millis(jsii.Number(15000)),
})
fi := awsapigatewayv2integrations.NewHttpLambdaIntegration(jsii.String("handlerIntegration"), f, &awsapigatewayv2integrations.HttpLambdaIntegrationProps{
PayloadFormatVersion: awscdkapigatewayv2alpha.PayloadFormatVersion_VERSION_2_0(),
})
endpoint := awsapigatewayv2.NewHttpApi(stack, jsii.String("storybookHttpApi"), &awsapigatewayv2.HttpApiProps{
DefaultIntegration: fi,
})
awscdk.NewCfnOutput(stack, jsii.String("storybookEndpointUrl"), &awscdk.CfnOutputProps{
ExportName: jsii.String("storybookEndpointUrl"),
Value: endpoint.Url(),
})
return stack
}
func main() {
app := awscdk.NewApp(nil)
NewCdkStack(app, "templStorybookExample", &CdkStackProps{
awscdk.StackProps{
Env: env(),
},
})
app.Synth(nil)
}
// env determines the AWS environment (account+region) in which our stack is to
// be deployed. For more information see: https://docs.aws.amazon.com/cdk/latest/guide/environments.html
func env() *awscdk.Environment {
// If unspecified, this stack will be "environment-agnostic".
// Account/Region-dependent features and context lookups will not work, but a
// single synthesized template can be deployed anywhere.
//---------------------------------------------------------------------------
return nil
// Uncomment if you know exactly what account and region you want to deploy
// the stack to. This is the recommendation for production stacks.
//---------------------------------------------------------------------------
// return &awscdk.Environment{
// Account: jsii.String("123456789012"),
// Region: jsii.String("us-east-1"),
// }
// Uncomment to specialize this stack for the AWS Account and Region that are
// implied by the current CLI configuration. This is recommended for dev
// stacks.
//---------------------------------------------------------------------------
// return &awscdk.Environment{
// Account: jsii.String(os.Getenv("CDK_DEFAULT_ACCOUNT")),
// Region: jsii.String(os.Getenv("CDK_DEFAULT_REGION")),
// }
}

View File

@@ -0,0 +1,18 @@
{
"app": "go mod download && go run cdk.go",
"context": {
"@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
"@aws-cdk/core:enableStackNameDuplicates": "true",
"aws-cdk:enableDiffNoFail": "true",
"@aws-cdk/core:stackRelativeExports": "true",
"@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true,
"@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true,
"@aws-cdk/aws-kms:defaultKeyPolicies": true,
"@aws-cdk/aws-s3:grantWriteWithoutAcl": true,
"@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true,
"@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
"@aws-cdk/aws-efs:defaultEncryptionAtRest": true,
"@aws-cdk/aws-lambda:recognizeVersionProps": true,
"@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true
}
}

View File

@@ -0,0 +1,4 @@
# Build the Storybook.
`cd ../lambda && go run main.go build`
# Deploy it.
cdk deploy

View File

@@ -0,0 +1,26 @@
module github.com/a-h/templ/storybook/example
go 1.23
toolchain go1.23.3
replace github.com/a-h/templ => ../../
require (
github.com/a-h/templ v0.0.0-00010101000000-000000000000
github.com/aws/aws-cdk-go/awscdk/v2 v2.25.0
github.com/aws/aws-cdk-go/awscdkapigatewayv2alpha/v2 v2.25.0-alpha.0
github.com/aws/aws-cdk-go/awscdkapigatewayv2integrationsalpha/v2 v2.25.0-alpha.0
github.com/aws/aws-cdk-go/awscdklambdagoalpha/v2 v2.25.0-alpha.0
github.com/aws/aws-lambda-go v1.27.0
github.com/aws/constructs-go/constructs/v10 v10.1.20
github.com/aws/jsii-runtime-go v1.59.0
)
require (
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/rs/cors v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/mod v0.20.0 // indirect
)

View File

@@ -0,0 +1,55 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/aws/aws-cdk-go/awscdk/v2 v2.25.0 h1:lTVj41TEVZBfKQ7btNSvBkCYuLw7Y60XXYpNBlhtjkM=
github.com/aws/aws-cdk-go/awscdk/v2 v2.25.0/go.mod h1:7XCtayiRILOHD/BkEyvxuqdrAHBt6dMXhSNcLm0ihU8=
github.com/aws/aws-cdk-go/awscdkapigatewayv2alpha/v2 v2.25.0-alpha.0 h1:5hqUcKS5O1p8LkYLFg0Mv0lA+t3+wz49AdDSSokBv3c=
github.com/aws/aws-cdk-go/awscdkapigatewayv2alpha/v2 v2.25.0-alpha.0/go.mod h1:kaKSDRgZeseFVVr3Qr4m+0XZ2U1idkkMjVsZbfG2XVE=
github.com/aws/aws-cdk-go/awscdkapigatewayv2integrationsalpha/v2 v2.25.0-alpha.0 h1:KTvckM8FJ/aeSq7oGWlMlO99YaXlP4VVEOJGGHW+BjI=
github.com/aws/aws-cdk-go/awscdkapigatewayv2integrationsalpha/v2 v2.25.0-alpha.0/go.mod h1:0gUU7nJB0/Y6ukGLQfcFKDX/MODXK37WBVltZl610sY=
github.com/aws/aws-cdk-go/awscdklambdagoalpha/v2 v2.25.0-alpha.0 h1:XZ0FpvZ4uPZ911REuISvTesA+71wllTCodhJa0p+9ug=
github.com/aws/aws-cdk-go/awscdklambdagoalpha/v2 v2.25.0-alpha.0/go.mod h1:cKT1tzcwvqHmRiih7eDFC0Kz1RsPys+727RkNbJAgaU=
github.com/aws/aws-lambda-go v1.27.0 h1:aLzrJwdyHoF1A18YeVdJjX8Ixkd+bpogdxVInvHcWjM=
github.com/aws/aws-lambda-go v1.27.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU=
github.com/aws/constructs-go/constructs/v10 v10.0.9/go.mod h1:RC6w8bOwxLmPX7Jfo9dkEZ9iVfgH4QnaVnfWvaNOHy0=
github.com/aws/constructs-go/constructs/v10 v10.1.20 h1:NQnZbzsssleUh+s10mRlRMSLDX0ETfZjLUEu6QxeBUg=
github.com/aws/constructs-go/constructs/v10 v10.1.20/go.mod h1:XFwFvzuX38hhTlpNVlC1tpgjCpRAAVr7a6+O0/9VB9c=
github.com/aws/jsii-runtime-go v1.37.0/go.mod h1:6tZnlstx8bAB3vnLFF9n8bbkI//LDblAek9zFyMXV3E=
github.com/aws/jsii-runtime-go v1.58.0/go.mod h1:OPeobFzUctDjq8EXbRZbIphpzQg3lzMs8KH09xuHyk0=
github.com/aws/jsii-runtime-go v1.59.0 h1:QEnIpd17oKv/UMFD2bPxLbT3B3S+QlYTmnPHEdKJkic=
github.com/aws/jsii-runtime-go v1.59.0/go.mod h1:OPeobFzUctDjq8EXbRZbIphpzQg3lzMs8KH09xuHyk0=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.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/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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=

View File

@@ -0,0 +1,89 @@
package main
import (
"context"
"embed"
"fmt"
"io"
"io/fs"
"net/http"
"net/http/httptest"
"os"
"github.com/a-h/templ/storybook/example"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
var s = example.Storybook()
func build() {
if err := s.Build(context.Background()); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
// Embed the build output into the Lambda.
// The build output is only 4MB, so there's plenty of space.
//
//go:embed storybook-server/storybook-static
var storybookStatic embed.FS
func run() {
// Replace the filesystem handler with the embedded data.
rooted, _ := fs.Sub(storybookStatic, "storybook-server/storybook-static")
s.StaticHandler = http.FileServer(http.FS(rooted))
// Start a Lambda handler.
lambda.Start(handler)
}
func handler(ctx context.Context, e events.APIGatewayV2HTTPRequest) (resp events.APIGatewayV2HTTPResponse, err error) {
// Record the result.
w := httptest.NewRecorder()
u := e.RawPath
if len(e.RawQueryString) > 0 {
u += "?" + e.RawQueryString
}
r := httptest.NewRequest(e.RequestContext.HTTP.Method, u, nil)
s.ServeHTTP(w, r)
// Convert it to an API Gateway response.
result := w.Result()
resp.StatusCode = result.StatusCode
bdy, err := io.ReadAll(w.Result().Body)
if err != nil {
return
}
resp.Body = string(bdy)
if len(result.Header) > 0 {
resp.Headers = make(map[string]string, len(result.Header))
for k := range result.Header {
v := result.Header.Get(k)
resp.Headers[k] = v
}
}
cookies := result.Cookies()
if len(cookies) > 0 {
resp.Cookies = make([]string, len(cookies))
for i := 0; i < len(cookies); i++ {
resp.Cookies[i] = cookies[i].String()
}
}
return
}
func main() {
if len(os.Args) < 2 {
run()
}
switch os.Args[1] {
case "build":
build()
case "run":
run()
default:
fmt.Printf("unexpected command %q\n", os.Args[1])
os.Exit(1)
}
}

View File

@@ -0,0 +1,17 @@
package main
import (
"context"
"fmt"
"os"
"github.com/a-h/templ/storybook/example"
)
func main() {
s := example.Storybook()
if err := s.ListenAndServeWithContext(context.Background()); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
}

View File

@@ -0,0 +1 @@
go run ./local/main.go

View File

@@ -0,0 +1,16 @@
package example
import (
"github.com/a-h/templ/storybook"
)
func Storybook() *storybook.Storybook {
s := storybook.New()
header := s.AddComponent("headerTemplate", headerTemplate, storybook.TextArg("name", "Page Name"))
header.AddStory("Long Name", storybook.TextArg("name", "A Very Long Page Name"))
s.AddComponent("footerTemplate", footerTemplate)
return s
}

View File

@@ -0,0 +1,19 @@
package example
import "fmt"
import "time"
templ headerTemplate(name string) {
<header data-testid="headerTemplate">
<h1>{ name }</h1>
</header>
}
templ footerTemplate() {
<footer data-testid="footerTemplate">
<div>&copy; { fmt.Sprintf("%d", time.Now().Year()) }</div>
</footer>
}

View File

@@ -0,0 +1,97 @@
// Code generated by templ - DO NOT EDIT.
package example
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "fmt"
import "time"
func headerTemplate(name string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<header data-testid=\"headerTemplate\"><h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `storybook/_example/templates.templ`, Line: 10, Col: 12}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h1></header>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
func footerTemplate() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<footer data-testid=\"footerTemplate\"><div>&copy; ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", time.Now().Year()))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `storybook/_example/templates.templ`, Line: 16, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -0,0 +1,7 @@
{
"name": "storybook-server",
"version": "1.0.0",
"main": "index.js",
"author": "",
"description": ""
}

View File

@@ -0,0 +1,540 @@
package storybook
import (
"bytes"
"context"
"encoding/json"
"fmt"
"math"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"strconv"
"strings"
"sync"
"golang.org/x/mod/sumdb/dirhash"
_ "embed"
"log/slog"
"github.com/a-h/templ"
"github.com/rs/cors"
)
type Storybook struct {
// Path to the storybook-server directory, defaults to ./storybook-server.
Path string
// RoutePrefix is the prefix of HTTP routes, e.g. /prod/
RoutePrefix string
// Config of the Stories.
Config map[string]*Conf
// Handlers for each of the components.
Handlers map[string]http.Handler
// Handler used to serve Storybook, defaults to filesystem at ./storybook-server/storybook-static.
StaticHandler http.Handler
Header string
Server http.Server
Log *slog.Logger
AdditionalPrefixJS string
}
type StorybookConfig func(*Storybook)
func WithServerAddr(addr string) StorybookConfig {
return func(sb *Storybook) {
sb.Server.Addr = addr
}
}
func WithHeader(header string) StorybookConfig {
return func(s *Storybook) {
s.Header = header
}
}
func WithPath(path string) StorybookConfig {
return func(sb *Storybook) {
sb.Path = path
}
}
// WithAdditionalPreviewJS / WithAdditionalPreviewJS allows to add content to the generated .storybook/preview.js file.
// For example this can be used to include custom CSS.
func WithAdditionalPreviewJS(content string) StorybookConfig {
return func(sb *Storybook) {
sb.AdditionalPrefixJS = content
}
}
func New(conf ...StorybookConfig) *Storybook {
logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))
sh := &Storybook{
Path: "./storybook-server",
Config: map[string]*Conf{},
Handlers: map[string]http.Handler{},
Log: logger,
}
sh.Server = http.Server{
Handler: sh,
Addr: ":60606",
}
for _, sc := range conf {
sc(sh)
}
// Depends on the correct Path, so must be set after additional config
sh.StaticHandler = http.FileServer(http.Dir(path.Join(sh.Path, "storybook-static")))
return sh
}
func (sh *Storybook) AddComponent(name string, componentConstructor any, args ...Arg) *Conf {
//TODO: Check that the component constructor is a function that returns a templ.Component.
c := NewConf(name, args...)
sh.Config[name] = c
h := NewHandler(name, componentConstructor, args...)
sh.Handlers[name] = h
return c
}
func (sh *Storybook) Build(ctx context.Context) (err error) {
// Download Storybook to the directory required.
sh.Log.Info("Installing storybook.")
err = sh.installStorybook()
if err != nil {
return
}
if ctx.Err() != nil {
return
}
// Copy the config to Storybook.
sh.Log.Info("Configuring storybook.")
configHasChanged, err := sh.configureStorybook()
if err != nil {
return
}
if ctx.Err() != nil {
return
}
// Execute a static build of storybook if the config has changed.
if configHasChanged {
sh.Log.Info("Config not present, or has changed, rebuilding storybook.")
err = sh.buildStorybook()
if err != nil {
return
}
} else {
sh.Log.Info("Storybook is up-to-date, skipping build step.")
}
if ctx.Err() != nil {
return
}
return
}
func (sh *Storybook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sbh := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, path.Join(sh.RoutePrefix, "/storybook_preview/")) {
sh.previewHandler(w, r)
return
}
sh.StaticHandler.ServeHTTP(w, r)
})
cors.Default().Handler(sbh).ServeHTTP(w, r)
}
func (sh *Storybook) ListenAndServeWithContext(ctx context.Context) (err error) {
err = sh.Build(ctx)
if err != nil {
return
}
go func() {
sh.Log.Info("Starting Go server", slog.String("address", sh.Server.Addr))
err = sh.Server.ListenAndServe()
}()
<-ctx.Done()
// Close the Go server.
sh.Server.Close()
return err
}
func (sh *Storybook) previewHandler(w http.ResponseWriter, r *http.Request) {
prefix := path.Join(sh.RoutePrefix, "/storybook_preview/")
if !strings.HasPrefix(r.URL.Path, prefix) {
sh.Log.Warn("URL does not match preview prefix", slog.String("url", r.URL.String()))
http.NotFound(w, r)
return
}
name, err := url.PathUnescape(strings.TrimPrefix(r.URL.Path, prefix))
if err != nil {
http.Error(w, fmt.Sprintf("failed to unescape URL: %v", err), http.StatusBadRequest)
return
}
if name == "" {
sh.Log.Warn("URL does not contain component name", slog.String("url", r.URL.String()))
http.NotFound(w, r)
return
}
name = strings.TrimPrefix(name, "/")
h, found := sh.Handlers[name]
if !found {
sh.Log.Info("Component name not found", slog.String("name", name), slog.String("url", r.URL.String()), slog.Any("available", keysOfMap(sh.Handlers)))
http.NotFound(w, r)
return
}
h.ServeHTTP(w, r)
}
func keysOfMap[K comparable, V any](handler map[K]V) (keys []K) {
keys = make([]K, len(handler))
var i int
for k := range handler {
keys[i] = k
i++
}
return keys
}
//go:embed _package.json
var packageJSON string
func (sh *Storybook) installStorybook() (err error) {
_, err = os.Stat(sh.Path)
if err == nil {
sh.Log.Info("Storybook already installed, Skipping installation.")
return
}
if os.IsNotExist(err) {
err = os.Mkdir(sh.Path, os.ModePerm)
if err != nil {
return fmt.Errorf("templ-storybook: error creating @storybook/server directory: %w", err)
}
err = os.WriteFile(filepath.Join(sh.Path, "package.json"), []byte(packageJSON), 0644)
if err != nil {
return fmt.Errorf("templ-storybook: error writing package.json: %w", err)
}
}
var cmd exec.Cmd
cmd.Dir = sh.Path
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Path, err = exec.LookPath("npx")
if err != nil {
return fmt.Errorf("templ-storybook: cannot install storybook, cannot find npx on the path, check that Node.js is installed: %w", err)
}
cmd.Args = []string{"npx", "sb", "init", "-t", "server", "--no-dev"}
return cmd.Run()
}
func (sh *Storybook) configureStorybook() (configHasChanged bool, err error) {
// Delete template/existing files in the stories directory.
storiesDir := filepath.Join(sh.Path, "stories")
before, err := dirhash.HashDir(storiesDir, "/", dirhash.DefaultHash)
if err != nil && !os.IsNotExist(err) {
return configHasChanged, err
}
if err = os.RemoveAll(storiesDir); err != nil {
return configHasChanged, err
}
if err := os.Mkdir(storiesDir, os.ModePerm); err != nil {
return configHasChanged, err
}
// Create new *.stories.json files.
for _, c := range sh.Config {
name := filepath.Join(sh.Path, fmt.Sprintf("stories/%s.stories.json", c.Title))
f, err := os.Create(name)
if err != nil {
return configHasChanged, fmt.Errorf("failed to create config file to %q: %w", name, err)
}
err = json.NewEncoder(f).Encode(c)
if err != nil {
return configHasChanged, fmt.Errorf("failed to write JSON config to %q: %w", name, err)
}
}
after, err := dirhash.HashDir(storiesDir, "/", dirhash.DefaultHash)
if err != nil {
return configHasChanged, fmt.Errorf("failed to hash directory %q: %w", storiesDir, err)
}
configHasChanged = before != after
// Configure storybook Preview URL.
err = os.WriteFile(filepath.Join(sh.Path, ".storybook/preview.js"), []byte(fmt.Sprintf("%s\n%s", sh.AdditionalPrefixJS, previewJS)), os.ModePerm)
if err != nil {
return
}
// Configure preview-head.html
err = os.WriteFile(filepath.Join(sh.Path, ".storybook/preview-head.html"), []byte(sh.Header), os.ModePerm)
return
}
var previewJS = `
// Customise fetch so that it uses a relative URL.
const fetchStoryHtml = async (url, path, params, context) => {
const qs = new URLSearchParams(params);
const response = await fetch("/storybook_preview/" + path + "?" + qs.toString());
return response.text();
};
export const parameters = {
server: {
url: "http://localhost/storybook_preview", // Ignored by fetchStoryHtml.
fetchStoryHtml,
},
};
`
func (sh *Storybook) buildStorybook() (err error) {
var cmd exec.Cmd
cmd.Dir = sh.Path
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Path, err = exec.LookPath("npm")
if err != nil {
return fmt.Errorf("templ-storybook: cannot run storybook, cannot find npm on the path, check that Node.js is installed: %w", err)
}
cmd.Args = []string{"npm", "run", "build-storybook"}
return cmd.Run()
}
func NewHandler(name string, f any, args ...Arg) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
argv := make([]any, len(args))
q := r.URL.Query()
for i, arg := range args {
argv[i] = arg.Get(q)
}
component, err := executeTemplate(name, f, argv)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
templ.Handler(component).ServeHTTP(w, r)
})
}
func executeTemplate(name string, fn any, values []any) (output templ.Component, err error) {
v := reflect.ValueOf(fn)
t := v.Type()
argv := make([]reflect.Value, t.NumIn())
if len(argv) != len(values) {
err = fmt.Errorf("templ-storybook: component %s expects %d argument, but %d were provided", fn, len(argv), len(values))
return
}
for i := 0; i < len(argv); i++ {
argv[i] = reflect.ValueOf(values[i])
}
result := v.Call(argv)
if len(result) != 1 {
err = fmt.Errorf("templ-storybook: function %s must return a templ.Component", name)
return
}
output, ok := result[0].Interface().(templ.Component)
if !ok {
err = fmt.Errorf("templ-storybook: result of function %s is not a templ.Component", name)
return
}
return output, nil
}
func NewConf(title string, args ...Arg) *Conf {
c := &Conf{
Title: title,
Parameters: StoryParameters{
Server: map[string]any{
"id": title,
},
},
Args: NewSortedMap(),
ArgTypes: NewSortedMap(),
Stories: []Story{},
}
for _, arg := range args {
c.Args.Add(arg.Name, arg.Value)
c.ArgTypes.Add(arg.Name, map[string]any{
"control": arg.Control,
})
}
c.AddStory("Default")
return c
}
func (c *Conf) AddStory(name string, args ...Arg) {
m := NewSortedMap()
for _, arg := range args {
m.Add(arg.Name, arg.Value)
}
c.Stories = append(c.Stories, Story{
Name: name,
Args: m,
})
}
// Controls for the configuration.
// See https://storybook.js.org/docs/react/essentials/controls
type Arg struct {
Name string
Value any
Control any
Get func(q url.Values) any
}
func ObjectArg(name string, value any, valuePtr any) Arg {
return Arg{
Name: name,
Value: value,
Control: "object",
Get: func(q url.Values) any {
err := json.Unmarshal([]byte(q.Get(name)), valuePtr)
if err != nil {
return err
}
return reflect.Indirect(reflect.ValueOf(valuePtr)).Interface()
},
}
}
func TextArg(name, value string) Arg {
return Arg{
Name: name,
Value: value,
Control: "text",
Get: func(q url.Values) any {
return q.Get(name)
},
}
}
func BooleanArg(name string, value bool) Arg {
return Arg{
Name: name,
Value: value,
Control: "boolean",
Get: func(q url.Values) any {
return q.Get(name) == "true"
},
}
}
type IntArgConf struct{ Min, Max, Step *int }
func IntArg(name string, value int, conf IntArgConf) Arg {
control := map[string]any{
"type": "number",
}
if conf.Min != nil {
control["min"] = conf.Min
}
if conf.Max != nil {
control["max"] = conf.Max
}
if conf.Step != nil {
control["step"] = conf.Step
}
arg := Arg{
Name: name,
Value: value,
Control: control,
Get: func(q url.Values) any {
i64, err := strconv.ParseInt(q.Get(name), 10, 64)
if err != nil || i64 < math.MinInt || i64 > math.MaxInt {
return 0
}
return int(i64)
},
}
return arg
}
func FloatArg(name string, value float64, min, max, step float64) Arg {
return Arg{
Name: name,
Value: value,
Control: map[string]any{
"type": "number",
"min": min,
"max": max,
"step": step,
},
Get: func(q url.Values) any {
i, _ := strconv.ParseFloat(q.Get(name), 64)
return i
},
}
}
type Conf struct {
Title string `json:"title"`
Parameters StoryParameters `json:"parameters"`
Args *SortedMap `json:"args"`
ArgTypes *SortedMap `json:"argTypes"`
Stories []Story `json:"stories"`
}
type StoryParameters struct {
Server map[string]any `json:"server"`
}
func NewSortedMap() *SortedMap {
return &SortedMap{
m: new(sync.Mutex),
internal: map[string]any{},
keys: []string{},
}
}
type SortedMap struct {
m *sync.Mutex
internal map[string]any
keys []string
}
func (sm *SortedMap) Add(key string, value any) {
sm.m.Lock()
defer sm.m.Unlock()
sm.keys = append(sm.keys, key)
sm.internal[key] = value
}
func (sm *SortedMap) MarshalJSON() (output []byte, err error) {
sm.m.Lock()
defer sm.m.Unlock()
b := new(bytes.Buffer)
b.WriteRune('{')
enc := json.NewEncoder(b)
for i, k := range sm.keys {
err = enc.Encode(k)
if err != nil {
return
}
_, err = b.WriteRune(':')
if err != nil {
return
}
err = enc.Encode(sm.internal[k])
if err != nil {
return
}
if i < len(sm.keys)-1 {
_, err = b.WriteRune(',')
if err != nil {
return
}
}
}
b.WriteRune('}')
return b.Bytes(), nil
}
type Story struct {
Name string `json:"name"`
Args *SortedMap `json:"args"`
}