diff --git a/go.mod b/go.mod index c5478ef00..0c432f7b4 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( go.uber.org/atomic v1.8.0 // indirect go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.17.0 + golang.org/x/mod v0.5.1 // indirect ) //replace github.com/a-h/lexical => /Users/adrian/github.com/a-h/lexical diff --git a/go.sum b/go.sum index aa57421b1..d886b74bb 100644 --- a/go.sum +++ b/go.sum @@ -56,15 +56,28 @@ go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/storybook/storybook.go b/storybook/storybook.go index eaef7fb61..05db9cd69 100644 --- a/storybook/storybook.go +++ b/storybook/storybook.go @@ -12,8 +12,11 @@ import ( "path" "reflect" "strconv" + "strings" "sync" + "golang.org/x/mod/sumdb/dirhash" + "github.com/a-h/pathvars" "github.com/a-h/templ" "github.com/rs/cors" @@ -45,7 +48,7 @@ func New() *Storybook { Log: logger, } sh.Server = http.Server{ - Handler: sh, + Handler: http.NotFoundHandler(), Addr: "localhost:60606", } return sh @@ -62,32 +65,10 @@ func (sh *Storybook) AddComponent(name string, componentConstructor interface{}, var storybookPreviewMatcher = pathvars.NewExtractor("/storybook_preview/{name}") -func (sh *Storybook) ServeHTTP(w http.ResponseWriter, r *http.Request) { - values, ok := storybookPreviewMatcher.Extract(r.URL) - if !ok { - sh.Log.Info("URL not matched", zap.String("url", r.URL.String())) - http.NotFound(w, r) - return - } - name, ok := values["name"] - if !ok { - sh.Log.Info("URL does not contain component name", zap.String("url", r.URL.String())) - http.NotFound(w, r) - return - } - h, found := sh.Handlers[name] - if !found { - sh.Log.Info("Component name not found", zap.String("name", name), zap.String("url", r.URL.String())) - http.NotFound(w, r) - return - } - cors.Default().Handler(h).ServeHTTP(w, r) -} - func (sh *Storybook) ListenAndServeWithContext(ctx context.Context) (err error) { defer sh.Log.Sync() // Download Storybook to the directory required. - sh.Log.Info("Installing storybook") + sh.Log.Info("Installing storybook.") err = sh.installStorybook() if err != nil { return @@ -97,8 +78,8 @@ func (sh *Storybook) ListenAndServeWithContext(ctx context.Context) (err error) } // Copy the config to Storybook. - sh.Log.Info("Configuring storybook") - err = sh.configureStorybook() + sh.Log.Info("Configuring storybook.") + configHasChanged, err := sh.configureStorybook() if err != nil { return } @@ -106,16 +87,30 @@ func (sh *Storybook) ListenAndServeWithContext(ctx context.Context) (err error) return } - // Run storybook. - go func() { - sh.Log.Info("Starting Storybook on http://localhost:6006/") - sh.runStorybook(ctx) - }() + // Execute a static build of storybook if the config has changed. + if configHasChanged { + sh.Log.Info("Config not present, or has changed, rebuilding storybook.") + sh.buildStorybook() + } else { + sh.Log.Info("Storybook is up-to-date, skipping build step.") + } + if ctx.Err() != nil { + return + } + + // Combine static and dynamic routes and start the server. + staticHandler := http.FileServer(http.Dir("./storybook-server/storybook-static")) + sbh := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/storybook_preview/") { + sh.previewHandler(w, r) + return + } + staticHandler.ServeHTTP(w, r) + }) + sh.Server.Handler = cors.Default().Handler(sbh) - // Start the Go server. - sh.Server.Handler = sh go func() { - sh.Log.Info("Starting Go server", zap.String("address", sh.Server.Addr), zap.String("serverUrl", sh.ServerURL)) + sh.Log.Info("Starting Go server", zap.String("address", sh.Server.Addr), zap.String("url", fmt.Sprintf("http://%s", sh.Server.Addr)), zap.String("previewUrl", sh.ServerURL)) err = sh.Server.ListenAndServe() }() <-ctx.Done() @@ -124,10 +119,32 @@ func (sh *Storybook) ListenAndServeWithContext(ctx context.Context) (err error) return err } +func (sh *Storybook) previewHandler(w http.ResponseWriter, r *http.Request) { + values, ok := storybookPreviewMatcher.Extract(r.URL) + if !ok { + sh.Log.Info("URL not matched", zap.String("url", r.URL.String())) + http.NotFound(w, r) + return + } + name, ok := values["name"] + if !ok { + sh.Log.Info("URL does not contain component name", zap.String("url", r.URL.String())) + http.NotFound(w, r) + return + } + h, found := sh.Handlers[name] + if !found { + sh.Log.Info("Component name not found", zap.String("name", name), zap.String("url", r.URL.String())) + http.NotFound(w, r) + return + } + h.ServeHTTP(w, r) +} + func (sh *Storybook) installStorybook() (err error) { _, err = os.Stat(sh.Path) if err == nil { - sh.Log.Info("Skipping installation - Storybook is already installed.") + sh.Log.Info("Storybook already installed, Skipping installation.") return } if os.IsNotExist(err) { @@ -148,30 +165,37 @@ func (sh *Storybook) installStorybook() (err error) { return cmd.Run() } -func (sh *Storybook) configureStorybook() error { +func (sh *Storybook) configureStorybook() (configHasChanged bool, err error) { // Delete template/existing files in the stories directory. storiesDir := path.Join(sh.Path, "stories") - if err := os.RemoveAll(storiesDir); err != nil { - return err + 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 err + return configHasChanged, err } // Create new *.stories.json files. for _, c := range sh.Config { name := path.Join(sh.Path, fmt.Sprintf("stories/%s.stories.json", c.Title)) f, err := os.Create(name) if err != nil { - return fmt.Errorf("failed to create config file to %q: %w", name, err) + return configHasChanged, fmt.Errorf("failed to create config file to %q: %w", name, err) } err = json.NewEncoder(f).Encode(c) if err != nil { - return fmt.Errorf("failed to write JSON config to %q: %w", name, err) + return configHasChanged, fmt.Errorf("failed to write JSON config to %q: %w", name, err) } } + after, err := dirhash.HashDir(storiesDir, "/", dirhash.DefaultHash) + configHasChanged = before != after // Configure storybook Preview URL. previewJS := fmt.Sprintf(`export const parameters = { server: { url: %s } };`, jsonEncode(sh.ServerURL)) - return os.WriteFile(path.Join(sh.Path, ".storybook/preview.js"), []byte(previewJS), os.ModePerm) + err = os.WriteFile(path.Join(sh.Path, ".storybook/preview.js"), []byte(previewJS), os.ModePerm) + return } func jsonEncode(s string) string { @@ -179,7 +203,20 @@ func jsonEncode(s string) string { return string(b) } -func (sh *Storybook) runStorybook(ctx context.Context) (err error) { +func (sh *Storybook) hasConfigChanged() (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", "storybook-build"} + return cmd.Run() +} + +func (sh *Storybook) buildStorybook() (err error) { var cmd exec.Cmd cmd.Dir = sh.Path cmd.Stdout = os.Stdout @@ -188,7 +225,7 @@ func (sh *Storybook) runStorybook(ctx context.Context) (err error) { 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", "storybook"} + cmd.Args = []string{"npm", "run", "build-storybook"} return cmd.Run() }