Changed: DB Params
This commit is contained in:
3
templ/storybook/.gitignore
vendored
Normal file
3
templ/storybook/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
storybook-server
|
||||
|
||||
*storybook.log
|
19
templ/storybook/_example/cdk/.gitignore
vendored
Normal file
19
templ/storybook/_example/cdk/.gitignore
vendored
Normal 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
|
14
templ/storybook/_example/cdk/README.md
Normal file
14
templ/storybook/_example/cdk/README.md
Normal 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
|
87
templ/storybook/_example/cdk/cdk.go
Normal file
87
templ/storybook/_example/cdk/cdk.go
Normal 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")),
|
||||
// }
|
||||
}
|
18
templ/storybook/_example/cdk/cdk.json
Normal file
18
templ/storybook/_example/cdk/cdk.json
Normal 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
|
||||
}
|
||||
}
|
4
templ/storybook/_example/cdk/deploy.sh
Executable file
4
templ/storybook/_example/cdk/deploy.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
# Build the Storybook.
|
||||
`cd ../lambda && go run main.go build`
|
||||
# Deploy it.
|
||||
cdk deploy
|
26
templ/storybook/_example/go.mod
Normal file
26
templ/storybook/_example/go.mod
Normal 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
|
||||
)
|
55
templ/storybook/_example/go.sum
Normal file
55
templ/storybook/_example/go.sum
Normal 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=
|
89
templ/storybook/_example/lambda/main.go
Normal file
89
templ/storybook/_example/lambda/main.go
Normal 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)
|
||||
}
|
||||
}
|
17
templ/storybook/_example/local/main.go
Normal file
17
templ/storybook/_example/local/main.go
Normal 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)
|
||||
}
|
||||
}
|
1
templ/storybook/_example/run.sh
Executable file
1
templ/storybook/_example/run.sh
Executable file
@@ -0,0 +1 @@
|
||||
go run ./local/main.go
|
16
templ/storybook/_example/storybook.go
Normal file
16
templ/storybook/_example/storybook.go
Normal 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
|
||||
}
|
19
templ/storybook/_example/templates.templ
Normal file
19
templ/storybook/_example/templates.templ
Normal 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>© { fmt.Sprintf("%d", time.Now().Year()) }</div>
|
||||
</footer>
|
||||
}
|
||||
|
97
templ/storybook/_example/templates_templ.go
Normal file
97
templ/storybook/_example/templates_templ.go
Normal 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>© ")
|
||||
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
|
7
templ/storybook/_package.json
Normal file
7
templ/storybook/_package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "storybook-server",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "",
|
||||
"description": ""
|
||||
}
|
540
templ/storybook/storybook.go
Normal file
540
templ/storybook/storybook.go
Normal 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"`
|
||||
}
|
Reference in New Issue
Block a user