From 5cdab36ad72530678fb62dd703a86813f6874534 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Tue, 21 Jan 2025 15:52:06 +1100
Subject: [PATCH 01/31] websites for nitric run
---
cmd/run.go | 26 ++++++
pkg/cloud/cloud.go | 5 +
pkg/cloud/websites/websites.go | 120 ++++++++++++++++++++++++
pkg/project/config.go | 14 +++
pkg/project/project.go | 110 ++++++++++++++++++++++
pkg/project/website.go | 141 +++++++++++++++++++++++++++++
pkg/view/tui/commands/local/run.go | 25 +++++
7 files changed, 441 insertions(+)
create mode 100644 pkg/cloud/websites/websites.go
create mode 100644 pkg/project/website.go
diff --git a/cmd/run.go b/cmd/run.go
index b690e59b6..6cbe38618 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -149,6 +149,23 @@ var runCmd = &cobra.Command{
tui.CheckErr(err)
}
+ websiteBuildUpdates, err := proj.BuildWebsites(loadEnv)
+ tui.CheckErr(err)
+
+ if isNonInteractive() {
+ fmt.Println("building project websites")
+ for update := range websiteBuildUpdates {
+ for _, line := range strings.Split(strings.TrimSuffix(update.Message, "\n"), "\n") {
+ fmt.Printf("%s [%s]: %s\n", update.ServiceName, update.Status, line)
+ }
+ }
+ } else {
+ prog := teax.NewProgram(build.NewModel(websiteBuildUpdates, "Building Websites"))
+ // blocks but quits once the above updates channel is closed by the build process
+ _, err = prog.Run()
+ tui.CheckErr(err)
+ }
+
// Run the app code (project services)
stopChan := make(chan bool)
updatesChan := make(chan project.ServiceRunUpdate)
@@ -179,6 +196,15 @@ var runCmd = &cobra.Command{
}
}()
+ go func() {
+ err := proj.RunWebsites(localCloud)
+ if err != nil {
+ localCloud.Stop()
+
+ tui.CheckErr(err)
+ }
+ }()
+
tui.CheckErr(err)
// FIXME: This is a hack to get labelled logs into the TUI
// We should refactor the system logs to be more generic
diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go
index 90163fa1c..a56213a7e 100644
--- a/pkg/cloud/cloud.go
+++ b/pkg/cloud/cloud.go
@@ -36,6 +36,7 @@ import (
"github.com/nitrictech/cli/pkg/cloud/sql"
"github.com/nitrictech/cli/pkg/cloud/storage"
"github.com/nitrictech/cli/pkg/cloud/topics"
+ "github.com/nitrictech/cli/pkg/cloud/websites"
"github.com/nitrictech/cli/pkg/cloud/websockets"
"github.com/nitrictech/cli/pkg/grpcx"
"github.com/nitrictech/cli/pkg/netx"
@@ -67,6 +68,7 @@ type LocalCloud struct {
Storage *storage.LocalStorageService
Topics *topics.LocalTopicsAndSubscribersService
Websockets *websockets.LocalWebsocketService
+ Websites *websites.LocalWebsiteService
Queues *queues.LocalQueuesService
Databases *sql.LocalSqlServer
}
@@ -315,6 +317,8 @@ func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) {
return nil, err
}
+ localWebsites := websites.NewLocalWebsitesService()
+
return &LocalCloud{
servers: make(map[string]*server.NitricServer),
Apis: localApis,
@@ -325,6 +329,7 @@ func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) {
Storage: localStorage,
Topics: localTopics,
Websockets: localWebsockets,
+ Websites: localWebsites,
Gateway: localGateway,
Secrets: localSecrets,
KeyValue: keyvalueService,
diff --git a/pkg/cloud/websites/websites.go b/pkg/cloud/websites/websites.go
new file mode 100644
index 000000000..ca58b27e8
--- /dev/null
+++ b/pkg/cloud/websites/websites.go
@@ -0,0 +1,120 @@
+// Copyright Nitric Pty Ltd.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at:
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package websites
+
+import (
+ "fmt"
+ "maps"
+ "net"
+ "net/http"
+ "sync"
+
+ "github.com/asaskevich/EventBus"
+ "github.com/nitrictech/cli/pkg/netx"
+ "github.com/nitrictech/nitric/core/pkg/logger"
+)
+
+type (
+ WebsiteName = string
+ State = map[WebsiteName]string
+)
+
+type LocalWebsiteService struct {
+ websiteRegLock sync.RWMutex
+ state State
+
+ bus EventBus.Bus
+}
+
+const localWebsitesTopic = "local_websites"
+
+func (l *LocalWebsiteService) publishState() {
+ l.bus.Publish(localWebsitesTopic, maps.Clone(l.state))
+}
+
+func (l *LocalWebsiteService) SubscribeToState(fn func(State)) {
+ // ignore the error, it's only returned if the fn param isn't a function
+ _ = l.bus.Subscribe(localWebsitesTopic, fn)
+}
+
+// register - Register a new website
+func (l *LocalWebsiteService) register(websiteName string, port int) {
+ if _, exists := l.state[websiteName]; exists {
+ logger.Warnf("Website %s is already registered", websiteName)
+ return
+ }
+
+ l.websiteRegLock.Lock()
+ defer l.websiteRegLock.Unlock()
+
+ l.state[websiteName] = fmt.Sprintf("http://localhost:%d", port)
+
+ l.publishState()
+}
+
+// deregister - Deregister a website
+func (l *LocalWebsiteService) deregister(websiteName string) {
+ l.websiteRegLock.Lock()
+ defer l.websiteRegLock.Unlock()
+
+ delete(l.state, websiteName)
+
+ l.publishState()
+}
+
+// Serve - Serve a website from the local filesystem
+func (l *LocalWebsiteService) Serve(websiteName string, path string) error {
+ // serve the website from path using http server
+ fs := http.FileServer(http.Dir(path))
+
+ // Create a new ServeMux to handle the request
+ mux := http.NewServeMux()
+ mux.Handle("/", fs)
+
+ // get an available port
+ ports, err := netx.TakePort(1)
+ if err != nil {
+ return err
+ }
+
+ port := ports[0] // Take the first available port
+
+ // Start the HTTP server on the assigned port
+ listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
+ if err != nil {
+ l.deregister(websiteName)
+ return fmt.Errorf("failed to start server on port %d: %w", port, err)
+ }
+
+ go func() {
+ if err := http.Serve(listener, mux); err != nil {
+ logger.Errorf("Error serving website %s: %s", websiteName, err.Error())
+ l.deregister(websiteName)
+ }
+ }()
+
+ l.register(websiteName, port)
+
+ return nil
+}
+
+func NewLocalWebsitesService() *LocalWebsiteService {
+ return &LocalWebsiteService{
+ state: State{},
+ bus: EventBus.New(),
+ }
+}
diff --git a/pkg/project/config.go b/pkg/project/config.go
index 6e815f01c..d71e2c963 100644
--- a/pkg/project/config.go
+++ b/pkg/project/config.go
@@ -84,12 +84,26 @@ type BatchConfiguration struct {
BaseServiceConfiguration `yaml:",inline"`
}
+type Build struct {
+ Command string `yaml:"command"`
+ Output string `yaml:"output"`
+}
+
+type WebsiteConfiguration struct {
+ BaseServiceConfiguration `yaml:",inline"`
+
+ Build Build `yaml:"build"`
+ IndexPage string `yaml:"index,omitempty"`
+ ErrorPage string `yaml:"error,omitempty"`
+}
+
type ProjectConfiguration struct {
Name string `yaml:"name"`
Directory string `yaml:"-"`
Services []ServiceConfiguration `yaml:"services"`
Ports map[string]int `yaml:"ports,omitempty"`
Batches []BatchConfiguration `yaml:"batch-services"`
+ Websites []WebsiteConfiguration `yaml:"websites"`
Runtimes map[string]RuntimeConfiguration `yaml:"runtimes,omitempty"`
Preview []preview.Feature `yaml:"preview,omitempty"`
}
diff --git a/pkg/project/project.go b/pkg/project/project.go
index 9177acf91..8b9e5870a 100644
--- a/pkg/project/project.go
+++ b/pkg/project/project.go
@@ -54,6 +54,7 @@ type Project struct {
services []Service
batches []Batch
+ websites []Website
}
func (p *Project) GetServices() []Service {
@@ -64,6 +65,10 @@ func (p *Project) GetBatchServices() []Batch {
return p.batches
}
+func (p *Project) GetWebsites() []Website {
+ return p.websites
+}
+
// TODO: Reduce duplicate code
// BuildBatches - Builds all the batches in the project
func (p *Project) BuildBatches(fs afero.Fs, useBuilder bool) (chan ServiceBuildUpdate, error) {
@@ -176,6 +181,61 @@ func (p *Project) BuildServices(fs afero.Fs, useBuilder bool) (chan ServiceBuild
return updatesChan, nil
}
+// BuildWebsites - Builds all the websites in the project via build command
+func (p *Project) BuildWebsites(env map[string]string) (chan ServiceBuildUpdate, error) {
+ updatesChan := make(chan ServiceBuildUpdate)
+
+ maxConcurrentBuilds := make(chan struct{}, min(goruntime.NumCPU(), goruntime.GOMAXPROCS(0)))
+
+ waitGroup := sync.WaitGroup{}
+
+ for _, website := range p.websites {
+ waitGroup.Add(1)
+ // Create writer
+ serviceBuildUpdateWriter := NewBuildUpdateWriter(website.Name, updatesChan)
+
+ go func(site Website, writer io.Writer) {
+ // Acquire a token by filling the maxConcurrentBuilds channel
+ // this will block once the buffer is full
+ maxConcurrentBuilds <- struct{}{}
+
+ // Start goroutine
+ if err := site.Build(updatesChan, env); err != nil {
+ updatesChan <- ServiceBuildUpdate{
+ ServiceName: site.Name,
+ Err: err,
+ Message: err.Error(),
+ Status: ServiceBuildStatus_Error,
+ }
+
+ } else {
+ updatesChan <- ServiceBuildUpdate{
+ ServiceName: site.Name,
+ Message: "Build Complete",
+ Status: ServiceBuildStatus_Complete,
+ }
+ }
+
+ // release our lock
+ <-maxConcurrentBuilds
+
+ waitGroup.Done()
+ }(website, serviceBuildUpdateWriter)
+ }
+
+ go func() {
+ waitGroup.Wait()
+ // Drain the semaphore to make sure all goroutines have finished
+ for i := 0; i < cap(maxConcurrentBuilds); i++ {
+ maxConcurrentBuilds <- struct{}{}
+ }
+
+ close(updatesChan)
+ }()
+
+ return updatesChan, nil
+}
+
func (p *Project) collectServiceRequirements(service Service) (*collector.ServiceRequirements, error) {
serviceRequirements := collector.NewServiceRequirements(service.Name, service.GetFilePath(), service.Type)
@@ -526,6 +586,27 @@ func (p *Project) RunServices(localCloud *cloud.LocalCloud, stop <-chan bool, up
return group.Wait()
}
+// RunWebsites - Runs all the websites as http servers
+// use the stop channel to stop all running websites
+func (p *Project) RunWebsites(localCloud *cloud.LocalCloud) error {
+ group, _ := errgroup.WithContext(context.TODO())
+
+ for _, site := range p.websites {
+ s := site
+
+ group.Go(func() error {
+ absoluteOutputPath, err := s.GetAbsoluteOutputPath()
+ if err != nil {
+ return err
+ }
+
+ return localCloud.Websites.Serve(s.Name, absoluteOutputPath)
+ })
+ }
+
+ return group.Wait()
+}
+
func (pc *ProjectConfiguration) pathToNormalizedServiceName(servicePath string) string {
// Add the project name as a prefix to group service images
servicePath = fmt.Sprintf("%s_%s", pc.Name, servicePath)
@@ -545,6 +626,7 @@ func (pc *ProjectConfiguration) pathToNormalizedServiceName(servicePath string)
func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *localconfig.LocalConfiguration, fs afero.Fs) (*Project, error) {
services := []Service{}
batches := []Batch{}
+ websites := []Website{}
matches := map[string]string{}
@@ -654,6 +736,33 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *
}
}
+ for _, websiteSpec := range projectConfig.Websites {
+ if websiteSpec.Build.Output == "" {
+ return nil, fmt.Errorf("no build output provided for website %s", websiteSpec.GetBasedir())
+ }
+
+ if websiteSpec.IndexPage == "" {
+ websiteSpec.IndexPage = "index.html"
+ }
+
+ if websiteSpec.ErrorPage == "" {
+ websiteSpec.ErrorPage = "index.html"
+ }
+
+ projectRelativeWebsiteFolder := filepath.Join(projectConfig.Directory, websiteSpec.GetBasedir())
+
+ websiteName := fmt.Sprintf("websites_%s", strings.ToLower(projectRelativeWebsiteFolder))
+
+ websites = append(websites, Website{
+ Name: websiteName,
+ basedir: websiteSpec.GetBasedir(),
+ outputPath: websiteSpec.Build.Output,
+ buildCmd: websiteSpec.Build.Command,
+ indexPage: websiteSpec.IndexPage,
+ errorPage: websiteSpec.ErrorPage,
+ })
+ }
+
// create an empty local configuration if none is provided
if localConfig == nil {
localConfig = &localconfig.LocalConfiguration{}
@@ -666,6 +775,7 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *
LocalConfig: *localConfig,
services: services,
batches: batches,
+ websites: websites,
}
if len(project.batches) > 0 && !slices.Contains(project.Preview, preview.Feature_BatchServices) {
diff --git a/pkg/project/website.go b/pkg/project/website.go
new file mode 100644
index 000000000..c039a3b32
--- /dev/null
+++ b/pkg/project/website.go
@@ -0,0 +1,141 @@
+// Copyright Nitric Pty Ltd.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at:
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package project
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+)
+
+type Website struct {
+ Name string
+
+ // the base directory for the website source files
+ basedir string
+
+ // the build command to build the website
+ buildCmd string
+
+ // the path to the website source files
+ outputPath string
+
+ // index page for the website
+ indexPage string
+
+ // error page for the website
+ errorPage string
+}
+
+func (s *Website) GetOutputPath() string {
+ return filepath.Join(s.basedir, s.outputPath)
+}
+
+func (s *Website) GetAbsoluteOutputPath() (string, error) {
+ return filepath.Abs(s.GetOutputPath())
+}
+
+// Run - runs the website using the provided command. TODO
+func (s *Website) Run(stop <-chan bool, updates chan<- ServiceRunUpdate, env map[string]string) error {
+ return nil
+}
+
+// Build - builds the website using the provided command
+func (s *Website) Build(updates chan ServiceBuildUpdate, env map[string]string) error {
+ if s.buildCmd == "" {
+ return fmt.Errorf("no build command provided for website %s", s.basedir)
+ }
+
+ commandParts := strings.Split(s.buildCmd, " ")
+ cmd := exec.Command(
+ commandParts[0],
+ commandParts[1:]...,
+ )
+
+ cmd.Env = append([]string{}, os.Environ()...)
+ cmd.Dir = s.basedir
+
+ for k, v := range env {
+ cmd.Env = append(cmd.Env, k+"="+v)
+ }
+
+ cmd.Stdout = &serviceBuildUpdateWriter{
+ buildUpdateChan: updates,
+ serviceName: s.Name,
+ }
+
+ cmd.Stderr = &serviceBuildUpdateWriter{
+ buildUpdateChan: updates,
+ serviceName: s.Name,
+ }
+
+ errChan := make(chan error)
+
+ go func() {
+ err := cmd.Start()
+ if err != nil {
+ errChan <- fmt.Errorf("error building website %s: %w", s.Name, err)
+ } else {
+ updates <- ServiceBuildUpdate{
+ ServiceName: s.Name,
+ Status: ServiceBuildStatus_InProgress,
+ Message: fmt.Sprintf("building website %s", s.GetOutputPath()),
+ }
+ }
+
+ err = cmd.Wait()
+ if err != nil {
+ // provide runtime errors as a run update rather than as a fatal error
+ updates <- ServiceBuildUpdate{
+ ServiceName: s.Name,
+ Status: ServiceBuildStatus_Error,
+ Err: err,
+ }
+ }
+
+ errChan <- nil
+ }()
+
+ // go func(cmd *exec.Cmd) {
+ // <-stop
+
+ // err := cmd.Process.Signal(syscall.SIGTERM)
+ // if err != nil {
+ // _ = cmd.Process.Kill()
+ // }
+ // }(cmd)
+
+ err := <-errChan
+
+ if err != nil {
+ updates <- ServiceBuildUpdate{
+ ServiceName: s.Name,
+ Status: ServiceBuildStatus_Error,
+ Err: err,
+ }
+ } else {
+ // updates <- ServiceBuildUpdate{
+ // ServiceName: s.Name,
+ // Status: ServiceBuildStatus_Complete,
+ // Message: fmt.Sprintf("website %s built successfully", s.Name),
+ // }
+ }
+
+ return err
+}
diff --git a/pkg/view/tui/commands/local/run.go b/pkg/view/tui/commands/local/run.go
index 5fc313001..7aac736a1 100644
--- a/pkg/view/tui/commands/local/run.go
+++ b/pkg/view/tui/commands/local/run.go
@@ -20,6 +20,7 @@ import (
"fmt"
"slices"
"sort"
+ "strings"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
@@ -32,6 +33,7 @@ import (
"github.com/nitrictech/cli/pkg/cloud/schedules"
"github.com/nitrictech/cli/pkg/cloud/sql"
"github.com/nitrictech/cli/pkg/cloud/topics"
+ "github.com/nitrictech/cli/pkg/cloud/websites"
"github.com/nitrictech/cli/pkg/cloud/websockets"
"github.com/nitrictech/cli/pkg/validation"
"github.com/nitrictech/cli/pkg/view/tui"
@@ -74,6 +76,11 @@ type DatabaseSummary struct {
status string
}
+type WebsiteSummary struct {
+ name string
+ url string
+}
+
type TuiModel struct {
localCloud *cloud.LocalCloud
apis []ApiSummary
@@ -82,6 +89,7 @@ type TuiModel struct {
topics []TopicSummary
schedules []ScheduleSummary
databases []DatabaseSummary
+ websites []WebsiteSummary
resources *resources.LocalResourcesState
@@ -103,6 +111,7 @@ func (t *TuiModel) Init() tea.Cmd {
reactive.ListenFor(t.reactiveSub, t.localCloud.Schedules.SubscribeToState)
reactive.ListenFor(t.reactiveSub, t.localCloud.Topics.SubscribeToState)
+ reactive.ListenFor(t.reactiveSub, t.localCloud.Websites.SubscribeToState)
return t.reactiveSub.AwaitNextMsg()
}
@@ -222,6 +231,17 @@ func (t *TuiModel) ReactiveUpdate(msg tea.Msg) (tea.Model, tea.Cmd) {
}
t.schedules = newSchedulesSummary
+ case websites.State:
+ newWebsitesSummary := []WebsiteSummary{}
+
+ for websiteName, url := range state {
+ newWebsitesSummary = append(newWebsitesSummary, WebsiteSummary{
+ name: strings.TrimPrefix(websiteName, "websites_"),
+ url: url,
+ })
+ }
+
+ t.websites = newWebsitesSummary
}
return t, t.reactiveSub.AwaitNextMsg()
@@ -289,6 +309,11 @@ func (t *TuiModel) View() string {
v.Addln(database.status).WithStyle(textHighlight)
}
+ for _, site := range t.websites {
+ v.Addf("site:%s - ", site.name)
+ v.Addln(site.url).WithStyle(textHighlight)
+ }
+
if t.resources != nil {
if len(t.resources.ServiceErrors) > 0 {
v.Break()
From 356647bcb852e1a9b0218dd9a4761dbda5b81897 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Tue, 21 Jan 2025 16:20:45 +1100
Subject: [PATCH 02/31] wip nitric start for websites
---
cmd/start.go | 10 +++++
pkg/project/config.go | 5 +++
pkg/project/project.go | 36 ++++++++++++++-
pkg/project/website.go | 99 ++++++++++++++++++++++++++++++++++--------
4 files changed, 132 insertions(+), 18 deletions(-)
diff --git a/cmd/start.go b/cmd/start.go
index 9de383df8..24097455b 100644
--- a/cmd/start.go
+++ b/cmd/start.go
@@ -238,6 +238,16 @@ var startCmd = &cobra.Command{
}
}()
+ // FIXME: Duplicate code
+ go func() {
+ err := proj.RunWebsitesWithCommand(localCloud, stopChan, updatesChan, localEnv)
+ if err != nil {
+ localCloud.Stop()
+
+ tui.CheckErr(err)
+ }
+ }()
+
// FIXME: This is a hack to get labelled logs into the TUI
// We should refactor the system logs to be more generic
systemChan := make(chan project.ServiceRunUpdate)
diff --git a/pkg/project/config.go b/pkg/project/config.go
index d71e2c963..add5f58c0 100644
--- a/pkg/project/config.go
+++ b/pkg/project/config.go
@@ -89,10 +89,15 @@ type Build struct {
Output string `yaml:"output"`
}
+type Dev struct {
+ Command string `yaml:"command"`
+}
+
type WebsiteConfiguration struct {
BaseServiceConfiguration `yaml:",inline"`
Build Build `yaml:"build"`
+ Dev Dev `yaml:"dev"`
IndexPage string `yaml:"index,omitempty"`
ErrorPage string `yaml:"error,omitempty"`
}
diff --git a/pkg/project/project.go b/pkg/project/project.go
index 8b9e5870a..e2348ae34 100644
--- a/pkg/project/project.go
+++ b/pkg/project/project.go
@@ -587,7 +587,6 @@ func (p *Project) RunServices(localCloud *cloud.LocalCloud, stop <-chan bool, up
}
// RunWebsites - Runs all the websites as http servers
-// use the stop channel to stop all running websites
func (p *Project) RunWebsites(localCloud *cloud.LocalCloud) error {
group, _ := errgroup.WithContext(context.TODO())
@@ -607,6 +606,40 @@ func (p *Project) RunWebsites(localCloud *cloud.LocalCloud) error {
return group.Wait()
}
+// RunWebsitesWithCommand - Runs all the websites using a startup command
+// use the stop channel to stop all running websites
+func (p *Project) RunWebsitesWithCommand(localCloud *cloud.LocalCloud, stop <-chan bool, updates chan<- ServiceRunUpdate, env map[string]string) error {
+ stopChannels := lo.FanOut[bool](len(p.websites), 1, stop)
+
+ group, _ := errgroup.WithContext(context.TODO())
+
+ for i, site := range p.websites {
+ idx := i
+ s := site
+
+ // start the service with the given file reference from its projects CWD
+ group.Go(func() error {
+ envVariables := map[string]string{
+ "PYTHONUNBUFFERED": "TRUE", // ensure all print statements print immediately for python
+ "NITRIC_ENVIRONMENT": "run",
+ }
+
+ for key, value := range env {
+ envVariables[key] = value
+ }
+
+ err := s.Run(stopChannels[idx], updates, envVariables)
+ if err != nil {
+ return fmt.Errorf("%s: %w", s.Name, err)
+ }
+
+ return nil
+ })
+ }
+
+ return group.Wait()
+}
+
func (pc *ProjectConfiguration) pathToNormalizedServiceName(servicePath string) string {
// Add the project name as a prefix to group service images
servicePath = fmt.Sprintf("%s_%s", pc.Name, servicePath)
@@ -758,6 +791,7 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *
basedir: websiteSpec.GetBasedir(),
outputPath: websiteSpec.Build.Output,
buildCmd: websiteSpec.Build.Command,
+ devCmd: websiteSpec.Dev.Command,
indexPage: websiteSpec.IndexPage,
errorPage: websiteSpec.ErrorPage,
})
diff --git a/pkg/project/website.go b/pkg/project/website.go
index c039a3b32..b079fa366 100644
--- a/pkg/project/website.go
+++ b/pkg/project/website.go
@@ -22,6 +22,7 @@ import (
"os/exec"
"path/filepath"
"strings"
+ "syscall"
)
type Website struct {
@@ -33,6 +34,9 @@ type Website struct {
// the build command to build the website
buildCmd string
+ // the dev command to run the website
+ devCmd string
+
// the path to the website source files
outputPath string
@@ -51,9 +55,85 @@ func (s *Website) GetAbsoluteOutputPath() (string, error) {
return filepath.Abs(s.GetOutputPath())
}
-// Run - runs the website using the provided command. TODO
+// Run - runs the website using the provided dev command
func (s *Website) Run(stop <-chan bool, updates chan<- ServiceRunUpdate, env map[string]string) error {
- return nil
+ if s.devCmd == "" {
+ return fmt.Errorf("no dev command provided for website %s", s.basedir)
+ }
+
+ commandParts := strings.Split(s.devCmd, " ")
+ cmd := exec.Command(
+ commandParts[0],
+ commandParts[1:]...,
+ )
+
+ cmd.Env = append([]string{}, os.Environ()...)
+ cmd.Dir = s.basedir
+
+ for k, v := range env {
+ cmd.Env = append(cmd.Env, k+"="+v)
+ }
+
+ cmd.Stdout = &ServiceRunUpdateWriter{
+ updates: updates,
+ serviceName: s.Name,
+ label: s.Name,
+ status: ServiceRunStatus_Running,
+ }
+
+ cmd.Stderr = &ServiceRunUpdateWriter{
+ updates: updates,
+ serviceName: s.Name,
+ label: s.Name,
+ status: ServiceRunStatus_Error,
+ }
+
+ errChan := make(chan error)
+
+ go func() {
+ err := cmd.Start()
+ if err != nil {
+ errChan <- fmt.Errorf("error starting website %s: %w", s.Name, err)
+ } else {
+ updates <- ServiceRunUpdate{
+ ServiceName: s.Name,
+ Label: "nitric",
+ Status: ServiceRunStatus_Running,
+ Message: fmt.Sprintf("started website %s", s.Name),
+ }
+ }
+
+ err = cmd.Wait()
+ if err != nil {
+ // provide runtime errors as a run update rather than as a fatal error
+ updates <- ServiceRunUpdate{
+ ServiceName: s.Name,
+ Label: "nitric",
+ Status: ServiceRunStatus_Error,
+ Err: err,
+ }
+ }
+
+ errChan <- nil
+ }()
+
+ go func(cmd *exec.Cmd) {
+ <-stop
+
+ err := cmd.Process.Signal(syscall.SIGTERM)
+ if err != nil {
+ _ = cmd.Process.Kill()
+ }
+ }(cmd)
+
+ err := <-errChan
+ updates <- ServiceRunUpdate{
+ ServiceName: s.Name,
+ Status: ServiceRunStatus_Error,
+ Err: err,
+ }
+
+ return err
}
// Build - builds the website using the provided command
@@ -112,15 +192,6 @@ func (s *Website) Build(updates chan ServiceBuildUpdate, env map[string]string)
errChan <- nil
}()
- // go func(cmd *exec.Cmd) {
- // <-stop
-
- // err := cmd.Process.Signal(syscall.SIGTERM)
- // if err != nil {
- // _ = cmd.Process.Kill()
- // }
- // }(cmd)
-
err := <-errChan
if err != nil {
@@ -129,12 +200,6 @@ func (s *Website) Build(updates chan ServiceBuildUpdate, env map[string]string)
Status: ServiceBuildStatus_Error,
Err: err,
}
- } else {
- // updates <- ServiceBuildUpdate{
- // ServiceName: s.Name,
- // Status: ServiceBuildStatus_Complete,
- // Message: fmt.Sprintf("website %s built successfully", s.Name),
- // }
}
return err
From 85062fd3af9c2f09f4f80fd040218e4af20c0c6d Mon Sep 17 00:00:00 2001
From: David Moore
Date: Thu, 23 Jan 2025 16:24:32 +1100
Subject: [PATCH 03/31] nitric run - single entrypoint with api proxy
---
cmd/stack.go | 17 ++++
pkg/cloud/cloud.go | 2 +-
pkg/cloud/websites/websites.go | 139 +++++++++++++++++++++++++--------
pkg/project/config.go | 2 +
pkg/project/project.go | 42 +++++++---
pkg/project/website.go | 3 +
6 files changed, 162 insertions(+), 43 deletions(-)
diff --git a/cmd/stack.go b/cmd/stack.go
index 276394605..221ea99cf 100644
--- a/cmd/stack.go
+++ b/cmd/stack.go
@@ -274,6 +274,23 @@ var stackUpdateCmd = &cobra.Command{
}
}
+ websiteBuildUpdates, err := proj.BuildWebsites(envVariables)
+ tui.CheckErr(err)
+
+ if isNonInteractive() {
+ fmt.Println("building project websites")
+ for update := range websiteBuildUpdates {
+ for _, line := range strings.Split(strings.TrimSuffix(update.Message, "\n"), "\n") {
+ fmt.Printf("%s [%s]: %s\n", update.ServiceName, update.Status, line)
+ }
+ }
+ } else {
+ prog := teax.NewProgram(build.NewModel(websiteBuildUpdates, "Building Websites"))
+ // blocks but quits once the above updates channel is closed by the build process
+ _, err = prog.Run()
+ tui.CheckErr(err)
+ }
+
providerStdout := make(chan string)
// Step 4. Start the deployment provider server
diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go
index a56213a7e..397fd3db4 100644
--- a/pkg/cloud/cloud.go
+++ b/pkg/cloud/cloud.go
@@ -317,7 +317,7 @@ func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) {
return nil, err
}
- localWebsites := websites.NewLocalWebsitesService()
+ localWebsites := websites.NewLocalWebsitesService(localGateway.GetApiAddress)
return &LocalCloud{
servers: make(map[string]*server.NitricServer),
diff --git a/pkg/cloud/websites/websites.go b/pkg/cloud/websites/websites.go
index ca58b27e8..73a1c6aa3 100644
--- a/pkg/cloud/websites/websites.go
+++ b/pkg/cloud/websites/websites.go
@@ -21,21 +21,36 @@ import (
"maps"
"net"
"net/http"
+ "net/http/httputil"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
"sync"
"github.com/asaskevich/EventBus"
"github.com/nitrictech/cli/pkg/netx"
- "github.com/nitrictech/nitric/core/pkg/logger"
)
+type Website struct {
+ Name string
+ BaseRoute string
+ OutputDir string
+ IndexPage string
+ ErrorPage string
+}
+
type (
- WebsiteName = string
- State = map[WebsiteName]string
+ WebsiteName = string
+ State = map[WebsiteName]string
+ GetApiAddress = func(apiName string) string
)
type LocalWebsiteService struct {
websiteRegLock sync.RWMutex
state State
+ port int
+ getApiAddress GetApiAddress
bus EventBus.Bus
}
@@ -52,16 +67,11 @@ func (l *LocalWebsiteService) SubscribeToState(fn func(State)) {
}
// register - Register a new website
-func (l *LocalWebsiteService) register(websiteName string, port int) {
- if _, exists := l.state[websiteName]; exists {
- logger.Warnf("Website %s is already registered", websiteName)
- return
- }
-
+func (l *LocalWebsiteService) register(website Website) {
l.websiteRegLock.Lock()
defer l.websiteRegLock.Unlock()
- l.state[websiteName] = fmt.Sprintf("http://localhost:%d", port)
+ l.state[website.Name] = fmt.Sprintf("http://localhost:%d/%s", l.port, strings.TrimPrefix(website.BaseRoute, "/"))
l.publishState()
}
@@ -76,45 +86,110 @@ func (l *LocalWebsiteService) deregister(websiteName string) {
l.publishState()
}
-// Serve - Serve a website from the local filesystem
-func (l *LocalWebsiteService) Serve(websiteName string, path string) error {
- // serve the website from path using http server
- fs := http.FileServer(http.Dir(path))
+type staticSiteHandler struct {
+ website Website
+ port int
+}
- // Create a new ServeMux to handle the request
- mux := http.NewServeMux()
- mux.Handle("/", fs)
+// ServeHTTP - Serve a static website from the local filesystem
+func (h staticSiteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ path := filepath.Join(h.website.OutputDir, r.URL.Path)
- // get an available port
- ports, err := netx.TakePort(1)
+ // check whether a file exists or is a directory at the given path
+ fi, err := os.Stat(path)
if err != nil {
- return err
+ if os.IsNotExist(err) {
+ // if the file doesn't exist, serve the error page with a 404 status code
+ http.ServeFile(w, r, filepath.Join(h.website.OutputDir, h.website.ErrorPage))
+ return
+ }
+
+ // if we got an error (that wasn't that the file doesn't exist) stating the
+ // file, return a 500 internal server error and stop
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ if fi.IsDir() {
+ http.ServeFile(w, r, filepath.Join(h.website.OutputDir, h.website.IndexPage))
+ return
}
- port := ports[0] // Take the first available port
+ // otherwise, use http.FileServer to serve the static file
+ http.FileServer(http.Dir(h.website.OutputDir)).ServeHTTP(w, r)
+}
- // Start the HTTP server on the assigned port
- listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
+// Serve - Serve a website from the local filesystem
+func (l *LocalWebsiteService) Start(websites []Website) error {
+ newLis, err := netx.GetNextListener(netx.MinPort(5000))
if err != nil {
- l.deregister(websiteName)
- return fmt.Errorf("failed to start server on port %d: %w", port, err)
+ return err
}
+ l.port = newLis.Addr().(*net.TCPAddr).Port
+
+ _ = newLis.Close()
+
+ // Initialize the multiplexer only if websites will be served
+ mux := http.NewServeMux()
+
+ // Register the API handler
+ mux.HandleFunc("/api/{name}/", func(w http.ResponseWriter, r *http.Request) {
+ // get the api name from the request path
+ apiName := r.PathValue("name")
+
+ // get the address of the api
+ apiAddress := l.getApiAddress(apiName)
+ if apiAddress == "" {
+ http.Error(w, fmt.Sprintf("api %s not found", apiName), http.StatusNotFound)
+ return
+ }
+
+ // Strip /api/{name}/ from the URL path
+ newPath := strings.TrimPrefix(r.URL.Path, fmt.Sprintf("/api/%s", apiName))
+
+ // Target backend API server
+ target, _ := url.Parse(apiAddress)
+
+ // Reverse proxy request
+ proxy := httputil.NewSingleHostReverseProxy(target)
+ r.URL.Path = newPath
+
+ // Forward the modified request to the backend
+ proxy.ServeHTTP(w, r)
+ })
+
+ // Register the SPA handler for each website
+ for _, website := range websites {
+ spa := staticSiteHandler{website: website, port: l.port}
+
+ if website.BaseRoute == "/" {
+ mux.Handle("/", spa)
+ } else {
+ mux.Handle(website.BaseRoute+"/", http.StripPrefix(website.BaseRoute+"/", spa))
+ }
+ }
+
+ // Start the server with the multiplexer
go func() {
- if err := http.Serve(listener, mux); err != nil {
- logger.Errorf("Error serving website %s: %s", websiteName, err.Error())
- l.deregister(websiteName)
+ addr := fmt.Sprintf(":%d", l.port)
+ if err := http.ListenAndServe(addr, mux); err != nil {
+ fmt.Printf("Failed to start server: %s\n", err)
}
}()
- l.register(websiteName, port)
+ // Register the websites
+ for _, website := range websites {
+ l.register(website)
+ }
return nil
}
-func NewLocalWebsitesService() *LocalWebsiteService {
+func NewLocalWebsitesService(getApiAddress GetApiAddress) *LocalWebsiteService {
return &LocalWebsiteService{
- state: State{},
- bus: EventBus.New(),
+ state: State{},
+ bus: EventBus.New(),
+ getApiAddress: getApiAddress,
}
}
diff --git a/pkg/project/config.go b/pkg/project/config.go
index add5f58c0..ad50ea26b 100644
--- a/pkg/project/config.go
+++ b/pkg/project/config.go
@@ -91,6 +91,7 @@ type Build struct {
type Dev struct {
Command string `yaml:"command"`
+ Url string `yaml:"url,omitempty"`
}
type WebsiteConfiguration struct {
@@ -98,6 +99,7 @@ type WebsiteConfiguration struct {
Build Build `yaml:"build"`
Dev Dev `yaml:"dev"`
+ Path string `yaml:"path"`
IndexPage string `yaml:"index,omitempty"`
ErrorPage string `yaml:"error,omitempty"`
}
diff --git a/pkg/project/project.go b/pkg/project/project.go
index e2348ae34..82c225b4a 100644
--- a/pkg/project/project.go
+++ b/pkg/project/project.go
@@ -39,6 +39,7 @@ import (
goruntime "runtime"
"github.com/nitrictech/cli/pkg/cloud"
+ "github.com/nitrictech/cli/pkg/cloud/websites"
"github.com/nitrictech/cli/pkg/collector"
"github.com/nitrictech/cli/pkg/preview"
"github.com/nitrictech/cli/pkg/project/localconfig"
@@ -588,22 +589,25 @@ func (p *Project) RunServices(localCloud *cloud.LocalCloud, stop <-chan bool, up
// RunWebsites - Runs all the websites as http servers
func (p *Project) RunWebsites(localCloud *cloud.LocalCloud) error {
- group, _ := errgroup.WithContext(context.TODO())
+ sites := []websites.Website{}
+ // register websites with the local cloud
for _, site := range p.websites {
- s := site
-
- group.Go(func() error {
- absoluteOutputPath, err := s.GetAbsoluteOutputPath()
- if err != nil {
- return err
- }
+ outputDir, err := site.GetAbsoluteOutputPath()
+ if err != nil {
+ return fmt.Errorf("unable to get absolute output path for website %s: %w", site.basedir, err)
+ }
- return localCloud.Websites.Serve(s.Name, absoluteOutputPath)
+ sites = append(sites, websites.Website{
+ Name: site.Name,
+ BaseRoute: site.path,
+ OutputDir: outputDir,
+ IndexPage: site.indexPage,
+ ErrorPage: site.errorPage,
})
}
- return group.Wait()
+ return localCloud.Websites.Start(sites)
}
// RunWebsitesWithCommand - Runs all the websites using a startup command
@@ -774,6 +778,10 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *
return nil, fmt.Errorf("no build output provided for website %s", websiteSpec.GetBasedir())
}
+ if websiteSpec.Path == "" {
+ websiteSpec.Path = "/" // default to root path
+ }
+
if websiteSpec.IndexPage == "" {
websiteSpec.IndexPage = "index.html"
}
@@ -789,6 +797,7 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *
websites = append(websites, Website{
Name: websiteName,
basedir: websiteSpec.GetBasedir(),
+ path: websiteSpec.Path,
outputPath: websiteSpec.Build.Output,
buildCmd: websiteSpec.Build.Command,
devCmd: websiteSpec.Dev.Command,
@@ -797,6 +806,19 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *
})
}
+ // check for duplicate paths in websites and error
+ siteDuplicates := lo.FindDuplicatesBy(websites, func(website Website) string {
+ return website.path
+ })
+
+ if len(siteDuplicates) > 0 {
+ duplicatePaths := lo.Map(siteDuplicates, func(website Website, i int) string {
+ return website.path
+ })
+
+ return nil, fmt.Errorf("duplicate website paths found: %s", strings.Join(duplicatePaths, ", "))
+ }
+
// create an empty local configuration if none is provided
if localConfig == nil {
localConfig = &localconfig.LocalConfiguration{}
diff --git a/pkg/project/website.go b/pkg/project/website.go
index b079fa366..281d7dc93 100644
--- a/pkg/project/website.go
+++ b/pkg/project/website.go
@@ -31,6 +31,9 @@ type Website struct {
// the base directory for the website source files
basedir string
+ // the path for the website subroutes, / is the root
+ path string
+
// the build command to build the website
buildCmd string
From 2269b4f1263f00e39f022d5354a9cec6808d39ea Mon Sep 17 00:00:00 2001
From: David Moore
Date: Fri, 31 Jan 2025 10:45:12 +1100
Subject: [PATCH 04/31] add to spec
---
cmd/debug.go | 5 ++++-
cmd/stack.go | 7 +++++--
go.mod | 10 ++++-----
go.sum | 28 ++++++++++++++-----------
pkg/cloud/websites/websites.go | 25 +++++++++++-----------
pkg/collector/spec.go | 22 +++++++++++++++++++-
pkg/project/project.go | 38 ++++++++++++++++++++++++++++------
7 files changed, 96 insertions(+), 39 deletions(-)
diff --git a/cmd/debug.go b/cmd/debug.go
index ba54ec65f..df3e960fc 100644
--- a/cmd/debug.go
+++ b/cmd/debug.go
@@ -100,6 +100,9 @@ var specCmd = &cobra.Command{
batchRequirements, err := proj.CollectBatchRequirements()
tui.CheckErr(err)
+ websiteRequirements, err := proj.CollectWebsiteRequirements()
+ tui.CheckErr(err)
+
additionalEnvFiles := []string{}
if debugEnvFile != "" {
@@ -115,7 +118,7 @@ var specCmd = &cobra.Command{
envVariables = map[string]string{}
}
- spec, err := collector.ServiceRequirementsToSpec(proj.Name, envVariables, serviceRequirements, batchRequirements)
+ spec, err := collector.ServiceRequirementsToSpec(proj.Name, envVariables, serviceRequirements, batchRequirements, websiteRequirements)
tui.CheckErr(err)
migrationImageContexts, err := collector.GetMigrationImageBuildContexts(serviceRequirements, batchRequirements, fs)
diff --git a/cmd/stack.go b/cmd/stack.go
index 221ea99cf..6cfbaaed1 100644
--- a/cmd/stack.go
+++ b/cmd/stack.go
@@ -220,6 +220,9 @@ var stackUpdateCmd = &cobra.Command{
batchRequirements, err := proj.CollectBatchRequirements()
tui.CheckErr(err)
+ websiteRequirements, err := proj.CollectWebsiteRequirements()
+ tui.CheckErr(err)
+
additionalEnvFiles := []string{}
if envFile != "" {
@@ -240,13 +243,13 @@ var stackUpdateCmd = &cobra.Command{
envVariables["NITRIC_BETA_PROVIDERS"] = "true"
}
- spec, err := collector.ServiceRequirementsToSpec(proj.Name, envVariables, serviceRequirements, batchRequirements)
+ spec, err := collector.ServiceRequirementsToSpec(proj.Name, envVariables, serviceRequirements, batchRequirements, websiteRequirements)
tui.CheckErr(err)
migrationImageContexts, err := collector.GetMigrationImageBuildContexts(serviceRequirements, batchRequirements, fs)
tui.CheckErr(err)
- // Build images from contexts and provide updates on the builds
+ // Build images from contexts and provide updates on the builds
if len(migrationImageContexts) > 0 {
migrationBuildUpdates, err := project.BuildMigrationImages(fs, migrationImageContexts, !noBuilder)
tui.CheckErr(err)
diff --git a/go.mod b/go.mod
index 2c0dff4a6..3e02fb68c 100644
--- a/go.mod
+++ b/go.mod
@@ -22,12 +22,12 @@ require (
github.com/hashicorp/consul/sdk v0.13.0
github.com/hashicorp/go-getter v1.6.2
github.com/hashicorp/go-version v1.7.0
- github.com/nitrictech/nitric/core v0.0.0-20241003062412-76ea6275fb0b
+ github.com/nitrictech/nitric/core v0.0.0-20250123074029-0306df1e20ae
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.8.1
github.com/valyala/fasthttp v1.55.0
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
- golang.org/x/mod v0.21.0 // indirect
+ golang.org/x/mod v0.22.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
google.golang.org/grpc v1.66.0
gopkg.in/yaml.v2 v2.4.0
@@ -115,7 +115,7 @@ require (
github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
- github.com/fatih/color v1.17.0 // indirect
+ github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/firefart/nonamedreturns v1.0.5 // indirect
@@ -281,12 +281,12 @@ require (
go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect
- golang.org/x/net v0.28.0 // indirect
+ golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.6.0 // indirect
- golang.org/x/tools v0.24.0 // indirect
+ golang.org/x/tools v0.27.0 // indirect
google.golang.org/api v0.196.0 // indirect
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
diff --git a/go.sum b/go.sum
index 2b83074cd..f3aaaafb1 100644
--- a/go.sum
+++ b/go.sum
@@ -81,8 +81,8 @@ github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJP
github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ=
github.com/Sereal/Sereal v0.0.0-20221130110801-16a4f76670cd h1:rP6LH3aVJTIxgTA3q79sSfnt8DvOlt17IRAklRBN+xo=
github.com/Sereal/Sereal v0.0.0-20221130110801-16a4f76670cd/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
-github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
-github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
+github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
+github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/go-check-sumtype v0.1.4 h1:WCvlB3l5Vq5dZQTFmodqL2g68uHiSwwlWcT5a2FGK0c=
github.com/alecthomas/go-check-sumtype v0.1.4/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
@@ -205,8 +205,8 @@ github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQt
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
-github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
-github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -585,8 +585,12 @@ github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm
github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
github.com/nitrictech/nitric/cloud/common v0.0.0-20241003062412-76ea6275fb0b h1:wZeUrnmhYjdhSuL6ov+kVfuFJC9H14sk0kzEpt6aRoo=
github.com/nitrictech/nitric/cloud/common v0.0.0-20241003062412-76ea6275fb0b/go.mod h1:ZsCdb3xbukhXAp9ZNbV6qWJqRC+eLkxhXy8bhs/cC2A=
-github.com/nitrictech/nitric/core v0.0.0-20241003062412-76ea6275fb0b h1:ImQFk66gRM3v9A6qmPImOiV3HJMDAX93X5rplMKn6ok=
-github.com/nitrictech/nitric/core v0.0.0-20241003062412-76ea6275fb0b/go.mod h1:9bQnYPqLzq8CcPk5MHT3phg19CWJhDlFOfdIv27lwwM=
+github.com/nitrictech/nitric/core v0.0.0-20250123065014-599bda2a2582 h1:dKVFR/rquvNB/FNu8GFAZJ5RcW7HrgyE+I8rdmvKwYc=
+github.com/nitrictech/nitric/core v0.0.0-20250123065014-599bda2a2582/go.mod h1:3kPpyO2oZEGfurDVsTh9XTg43b/4JAIbLkaktEvRF58=
+github.com/nitrictech/nitric/core v0.0.0-20250123070044-4b31d7498e96 h1:GdRQEkMYrZehM2SKkUecR2/SPa5TuKpxuuZetShZnAI=
+github.com/nitrictech/nitric/core v0.0.0-20250123070044-4b31d7498e96/go.mod h1:3kPpyO2oZEGfurDVsTh9XTg43b/4JAIbLkaktEvRF58=
+github.com/nitrictech/nitric/core v0.0.0-20250123074029-0306df1e20ae h1:hpSGt8KQ4OPPLqGvf5sFTv7h9isngWQLveVcfW4Z5i4=
+github.com/nitrictech/nitric/core v0.0.0-20250123074029-0306df1e20ae/go.mod h1:3kPpyO2oZEGfurDVsTh9XTg43b/4JAIbLkaktEvRF58=
github.com/nunnatsa/ginkgolinter v0.16.2 h1:8iLqHIZvN4fTLDC0Ke9tbSZVcyVHoBs0HIbnVSxfHJk=
github.com/nunnatsa/ginkgolinter v0.16.2/go.mod h1:4tWRinDN1FeJgU+iJANW/kz7xKN5nYRAOfJDQUS9dOQ=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
@@ -897,8 +901,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
-golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
+golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -940,8 +944,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
-golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1115,8 +1119,8 @@ golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
-golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
+golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
+golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/pkg/cloud/websites/websites.go b/pkg/cloud/websites/websites.go
index 73a1c6aa3..5e8b8d772 100644
--- a/pkg/cloud/websites/websites.go
+++ b/pkg/cloud/websites/websites.go
@@ -30,14 +30,15 @@ import (
"github.com/asaskevich/EventBus"
"github.com/nitrictech/cli/pkg/netx"
+ deploymentspb "github.com/nitrictech/nitric/core/pkg/proto/deployments/v1"
)
+type WebsitePb = deploymentspb.Website
+
type Website struct {
- Name string
- BaseRoute string
- OutputDir string
- IndexPage string
- ErrorPage string
+ WebsitePb
+
+ Name string
}
type (
@@ -71,7 +72,7 @@ func (l *LocalWebsiteService) register(website Website) {
l.websiteRegLock.Lock()
defer l.websiteRegLock.Unlock()
- l.state[website.Name] = fmt.Sprintf("http://localhost:%d/%s", l.port, strings.TrimPrefix(website.BaseRoute, "/"))
+ l.state[website.Name] = fmt.Sprintf("http://localhost:%d/%s", l.port, strings.TrimPrefix(website.BasePath, "/"))
l.publishState()
}
@@ -93,14 +94,14 @@ type staticSiteHandler struct {
// ServeHTTP - Serve a static website from the local filesystem
func (h staticSiteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- path := filepath.Join(h.website.OutputDir, r.URL.Path)
+ path := filepath.Join(h.website.OutputDirectory, r.URL.Path)
// check whether a file exists or is a directory at the given path
fi, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
// if the file doesn't exist, serve the error page with a 404 status code
- http.ServeFile(w, r, filepath.Join(h.website.OutputDir, h.website.ErrorPage))
+ http.ServeFile(w, r, filepath.Join(h.website.OutputDirectory, h.website.ErrorDocument))
return
}
@@ -111,12 +112,12 @@ func (h staticSiteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
if fi.IsDir() {
- http.ServeFile(w, r, filepath.Join(h.website.OutputDir, h.website.IndexPage))
+ http.ServeFile(w, r, filepath.Join(h.website.OutputDirectory, h.website.IndexDocument))
return
}
// otherwise, use http.FileServer to serve the static file
- http.FileServer(http.Dir(h.website.OutputDir)).ServeHTTP(w, r)
+ http.FileServer(http.Dir(h.website.OutputDirectory)).ServeHTTP(w, r)
}
// Serve - Serve a website from the local filesystem
@@ -163,10 +164,10 @@ func (l *LocalWebsiteService) Start(websites []Website) error {
for _, website := range websites {
spa := staticSiteHandler{website: website, port: l.port}
- if website.BaseRoute == "/" {
+ if website.BasePath == "/" {
mux.Handle("/", spa)
} else {
- mux.Handle(website.BaseRoute+"/", http.StripPrefix(website.BaseRoute+"/", spa))
+ mux.Handle(website.BasePath+"/", http.StripPrefix(website.BasePath+"/", spa))
}
}
diff --git a/pkg/collector/spec.go b/pkg/collector/spec.go
index e094e4d65..2441b9534 100644
--- a/pkg/collector/spec.go
+++ b/pkg/collector/spec.go
@@ -24,6 +24,8 @@ import (
"errors"
"fmt"
"net/url"
+ "os"
+ "path/filepath"
"regexp"
"slices"
"strings"
@@ -1042,7 +1044,7 @@ func checkServiceRequirementErrors(allServiceRequirements []*ServiceRequirements
}
// convert service requirements to a cloud bill of materials
-func ServiceRequirementsToSpec(projectName string, environmentVariables map[string]string, allServiceRequirements []*ServiceRequirements, allBatchRequirements []*BatchRequirements) (*deploymentspb.Spec, error) {
+func ServiceRequirementsToSpec(projectName string, environmentVariables map[string]string, allServiceRequirements []*ServiceRequirements, allBatchRequirements []*BatchRequirements, websiteRequirements []*deploymentspb.Website) (*deploymentspb.Spec, error) {
if err := checkServiceRequirementErrors(allServiceRequirements, allBatchRequirements); err != nil {
return nil, err
}
@@ -1180,6 +1182,24 @@ func ServiceRequirementsToSpec(projectName string, environmentVariables map[stri
})
}
+ for _, website := range websiteRequirements {
+ cleanedPath := strings.TrimRight(website.OutputDirectory, string(os.PathSeparator))
+ // Get the parent directory
+ parentDir := filepath.Dir(cleanedPath)
+ // Extract the directory name from the parent path
+ _, name := filepath.Split(parentDir)
+
+ newSpec.Resources = append(newSpec.Resources, &deploymentspb.Resource{
+ Id: &resourcespb.ResourceIdentifier{
+ Name: name,
+ Type: resourcespb.ResourceType_Website,
+ },
+ Config: &deploymentspb.Resource_Website{
+ Website: website,
+ },
+ })
+ }
+
return newSpec, projectErrors.Error()
}
diff --git a/pkg/project/project.go b/pkg/project/project.go
index 82c225b4a..7438a41b3 100644
--- a/pkg/project/project.go
+++ b/pkg/project/project.go
@@ -45,6 +45,7 @@ import (
"github.com/nitrictech/cli/pkg/project/localconfig"
"github.com/nitrictech/cli/pkg/project/runtime"
"github.com/nitrictech/nitric/core/pkg/logger"
+ deploymentpb "github.com/nitrictech/nitric/core/pkg/proto/deployments/v1"
)
type Project struct {
@@ -451,6 +452,27 @@ func (p *Project) CollectBatchRequirements() ([]*collector.BatchRequirements, er
return allBatchRequirements, nil
}
+func (p *Project) CollectWebsiteRequirements() ([]*deploymentpb.Website, error) {
+ allWebsiteRequirements := []*deploymentpb.Website{}
+
+ for _, site := range p.websites {
+ outputDir, err := site.GetAbsoluteOutputPath()
+ if err != nil {
+ return nil, fmt.Errorf("unable to get absolute output path for website %s: %w", site.basedir, err)
+ }
+
+ allWebsiteRequirements = append(allWebsiteRequirements, &deploymentpb.Website{
+ BasePath: site.path,
+ OutputDirectory: outputDir,
+ IndexDocument: site.indexPage,
+ ErrorDocument: site.errorPage,
+ })
+
+ }
+
+ return allWebsiteRequirements, nil
+}
+
// DefaultMigrationImage - Returns the default migration image name for the project
// Also returns ok if image is required or not
func (p *Project) DefaultMigrationImage(fs afero.Fs) (string, bool) {
@@ -588,6 +610,7 @@ func (p *Project) RunServices(localCloud *cloud.LocalCloud, stop <-chan bool, up
}
// RunWebsites - Runs all the websites as http servers
+// TODO this has duplicate code with CollectWebsiteRequirements
func (p *Project) RunWebsites(localCloud *cloud.LocalCloud) error {
sites := []websites.Website{}
@@ -599,11 +622,13 @@ func (p *Project) RunWebsites(localCloud *cloud.LocalCloud) error {
}
sites = append(sites, websites.Website{
- Name: site.Name,
- BaseRoute: site.path,
- OutputDir: outputDir,
- IndexPage: site.indexPage,
- ErrorPage: site.errorPage,
+ Name: site.Name,
+ WebsitePb: websites.WebsitePb{
+ BasePath: site.outputPath,
+ OutputDirectory: outputDir,
+ IndexDocument: site.indexPage,
+ ErrorDocument: site.errorPage,
+ },
})
}
@@ -778,8 +803,9 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *
return nil, fmt.Errorf("no build output provided for website %s", websiteSpec.GetBasedir())
}
+ // apply defaults
if websiteSpec.Path == "" {
- websiteSpec.Path = "/" // default to root path
+ websiteSpec.Path = "/"
}
if websiteSpec.IndexPage == "" {
From 6cfcf3c89e718877ae14da4e99b39821ed0bb9c3 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Tue, 11 Feb 2025 16:51:58 +1100
Subject: [PATCH 05/31] websites in dashboard wip
---
pkg/cloud/websites/websites.go | 5 +-
pkg/dashboard/dashboard.go | 29 +++
.../components/layout/AppLayout/AppLayout.tsx | 6 +
.../src/components/websites/SiteExplorer.tsx | 171 ++++++++++++++++++
.../src/components/websites/SiteTreeView.tsx | 64 +++++++
.../frontend/src/pages/websites.astro | 8 +
pkg/dashboard/frontend/src/types.ts | 6 +
pkg/project/project.go | 2 +-
8 files changed, 288 insertions(+), 3 deletions(-)
create mode 100644 pkg/dashboard/frontend/src/components/websites/SiteExplorer.tsx
create mode 100644 pkg/dashboard/frontend/src/components/websites/SiteTreeView.tsx
create mode 100644 pkg/dashboard/frontend/src/pages/websites.astro
diff --git a/pkg/cloud/websites/websites.go b/pkg/cloud/websites/websites.go
index 5e8b8d772..b83f9f33a 100644
--- a/pkg/cloud/websites/websites.go
+++ b/pkg/cloud/websites/websites.go
@@ -88,7 +88,7 @@ func (l *LocalWebsiteService) deregister(websiteName string) {
}
type staticSiteHandler struct {
- website Website
+ website *Website
port int
}
@@ -161,7 +161,8 @@ func (l *LocalWebsiteService) Start(websites []Website) error {
})
// Register the SPA handler for each website
- for _, website := range websites {
+ for i := range websites {
+ website := &websites[i]
spa := staticSiteHandler{website: website, port: l.port}
if website.BasePath == "/" {
diff --git a/pkg/dashboard/dashboard.go b/pkg/dashboard/dashboard.go
index 0f6061435..902c4e547 100644
--- a/pkg/dashboard/dashboard.go
+++ b/pkg/dashboard/dashboard.go
@@ -52,6 +52,7 @@ import (
"github.com/nitrictech/cli/pkg/cloud/sql"
"github.com/nitrictech/cli/pkg/cloud/storage"
"github.com/nitrictech/cli/pkg/cloud/topics"
+ "github.com/nitrictech/cli/pkg/cloud/websites"
"github.com/nitrictech/cli/pkg/cloud/websockets"
"github.com/nitrictech/cli/pkg/project"
"github.com/nitrictech/cli/pkg/update"
@@ -156,6 +157,11 @@ type HttpProxySpec struct {
Target string `json:"target"`
}
+type WebsiteSpec struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+}
+
type Dashboard struct {
resourcesLock sync.Mutex
project *project.Project
@@ -174,6 +180,7 @@ type Dashboard struct {
secrets []*SecretSpec
sqlDatabases []*SQLDatabaseSpec
websockets []WebsocketSpec
+ websites []WebsiteSpec
subscriptions []*SubscriberSpec
notifications []*NotifierSpec
httpProxies []*HttpProxySpec
@@ -201,6 +208,7 @@ type DashboardResponse struct {
Schedules []ScheduleSpec `json:"schedules"`
Topics []*TopicSpec `json:"topics"`
Websockets []WebsocketSpec `json:"websockets"`
+ Websites []WebsiteSpec `json:"websites"`
Subscriptions []*SubscriberSpec `json:"subscriptions"`
Notifications []*NotifierSpec `json:"notifications"`
Stores []*KeyValueSpec `json:"stores"`
@@ -630,6 +638,24 @@ func (d *Dashboard) updateSqlDatabases(state sql.State) {
d.refresh()
}
+func (d *Dashboard) handleWebsites(state websites.State) {
+ d.resourcesLock.Lock()
+ defer d.resourcesLock.Unlock()
+
+ websites := []WebsiteSpec{}
+
+ for name, url := range state {
+ websites = append(websites, WebsiteSpec{
+ Name: strings.TrimPrefix(name, "websites_"),
+ URL: url,
+ })
+ }
+
+ d.websites = websites
+
+ d.refresh()
+}
+
func (d *Dashboard) refresh() {
if !d.noBrowser && !d.browserHasOpened {
d.openBrowser()
@@ -826,6 +852,7 @@ func (d *Dashboard) sendStackUpdate() error {
SQLDatabases: d.sqlDatabases,
Schedules: d.schedules,
Websockets: d.websockets,
+ Websites: d.websites,
Policies: d.policies,
Queues: d.queues,
Secrets: d.secrets,
@@ -913,6 +940,7 @@ func New(noBrowser bool, localCloud *cloud.LocalCloud, project *project.Project)
subscriptions: []*SubscriberSpec{},
notifications: []*NotifierSpec{},
websockets: []WebsocketSpec{},
+ websites: []WebsiteSpec{},
stores: []*KeyValueSpec{},
sqlDatabases: []*SQLDatabaseSpec{},
secrets: []*SecretSpec{},
@@ -943,6 +971,7 @@ func New(noBrowser bool, localCloud *cloud.LocalCloud, project *project.Project)
localCloud.Storage.SubscribeToState(dash.updateBucketNotifications)
localCloud.Http.SubscribeToState(dash.updateHttpProxies)
localCloud.Databases.SubscribeToState(dash.updateSqlDatabases)
+ localCloud.Websites.SubscribeToState(dash.handleWebsites)
// subscribe to history events from gateway
localCloud.Apis.SubscribeToAction(dash.handleApiHistory)
diff --git a/pkg/dashboard/frontend/src/components/layout/AppLayout/AppLayout.tsx b/pkg/dashboard/frontend/src/components/layout/AppLayout/AppLayout.tsx
index 94116d507..07f0d31b0 100644
--- a/pkg/dashboard/frontend/src/components/layout/AppLayout/AppLayout.tsx
+++ b/pkg/dashboard/frontend/src/components/layout/AppLayout/AppLayout.tsx
@@ -15,6 +15,7 @@ import {
CircleStackIcon,
LockClosedIcon,
CpuChipIcon,
+ WindowIcon,
} from '@heroicons/react/24/outline'
import { cn } from '@/lib/utils'
import { useWebSocket } from '../../../lib/hooks/use-web-socket'
@@ -147,6 +148,11 @@ const AppLayout: React.FC = ({
href: '/websockets',
icon: ChatBubbleLeftRightIcon,
},
+ {
+ name: 'Websites',
+ href: '/websites',
+ icon: WindowIcon,
+ },
// { name: "Key Value Stores", href: "#", icon: FolderIcon, current: false },
]
diff --git a/pkg/dashboard/frontend/src/components/websites/SiteExplorer.tsx b/pkg/dashboard/frontend/src/components/websites/SiteExplorer.tsx
new file mode 100644
index 000000000..d34a04350
--- /dev/null
+++ b/pkg/dashboard/frontend/src/components/websites/SiteExplorer.tsx
@@ -0,0 +1,171 @@
+import { useEffect, useState } from 'react'
+
+import { Loading } from '../shared'
+import { useWebSocket } from '../../lib/hooks/use-web-socket'
+import AppLayout from '../layout/AppLayout'
+import type { Website } from '@/types'
+import BreadCrumbs from '../layout/BreadCrumbs'
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '../ui/select'
+import NotFoundAlert from '../shared/NotFoundAlert'
+import SiteTreeView from './SiteTreeView'
+import { Button } from '../ui/button'
+
+const LOCAL_STORAGE_KEY = 'nitric-local-dash-storage-history'
+
+const SiteExplorer = () => {
+ const [selectedWebsite, setSelectedWebsite] = useState()
+ const { data, loading } = useWebSocket()
+
+ const { websites } = data || {}
+
+ useEffect(() => {
+ if (websites?.length && !selectedWebsite) {
+ const previousWebsite = localStorage.getItem(
+ `${LOCAL_STORAGE_KEY}-last-website`,
+ )
+
+ setSelectedWebsite(
+ websites.find((b) => b.name === previousWebsite) || websites[0],
+ )
+ }
+ }, [websites])
+
+ useEffect(() => {
+ if (selectedWebsite) {
+ // set history
+ localStorage.setItem(
+ `${LOCAL_STORAGE_KEY}-last-website`,
+ selectedWebsite.name,
+ )
+ }
+ }, [selectedWebsite])
+
+ return (
+
+
+ Websites
+
+ {
+ setSelectedWebsite(b)
+ }}
+ websites={websites}
+ />
+ >
+ )
+ }
+ >
+
+ {websites && selectedWebsite ? (
+
+
+
+
+
+ {!data?.websites.some(
+ (s) => s.name === selectedWebsite.name,
+ ) && (
+
+ Website not found. It might have been updated or removed.
+ Select another website.
+
+ )}
+
+
+
+
{
+ setSelectedWebsite(websites.find((b) => b.name === name))
+ }}
+ >
+
+
+
+
+
+ {websites.map((website) => (
+
+ {website.name}
+
+ ))}
+
+
+
+
+
+ Open in a new tab
+
+
+
+
+
+
+ {selectedWebsite.url}
+
+
+
+
+
+
+ ) : !websites?.length ? (
+
+
+ Please refer to our documentation on{' '}
+
+ creating websites
+ {' '}
+ as we are unable to find any existing websites.
+
+
+ ) : null}
+
+
+ )
+}
+
+export default SiteExplorer
diff --git a/pkg/dashboard/frontend/src/components/websites/SiteTreeView.tsx b/pkg/dashboard/frontend/src/components/websites/SiteTreeView.tsx
new file mode 100644
index 000000000..bd02b24d2
--- /dev/null
+++ b/pkg/dashboard/frontend/src/components/websites/SiteTreeView.tsx
@@ -0,0 +1,64 @@
+import { type FC, useMemo } from 'react'
+import TreeView, { type TreeItemType } from '../shared/TreeView'
+import type { TreeItem, TreeItemIndex } from 'react-complex-tree'
+import type { Website, Notification } from '@/types'
+import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'
+import { Badge } from '../ui/badge'
+import { cn } from '@/lib/utils/cn'
+
+export type SiteTreeItemType = TreeItemType
+
+interface Props {
+ websites: Website[]
+ onSelect: (website: Website) => void
+ initialItem: Website
+}
+
+const SiteTreeView: FC = ({ websites, onSelect, initialItem }) => {
+ const treeItems: Record<
+ TreeItemIndex,
+ TreeItem
+ > = useMemo(() => {
+ const rootItem: TreeItem = {
+ index: 'root',
+ isFolder: true,
+ children: [],
+ data: null,
+ }
+
+ const rootItems: Record> = {
+ root: rootItem,
+ }
+
+ for (const website of websites) {
+ // add api if not added already
+ rootItems[website.name] = {
+ index: website.name,
+ data: {
+ label: website.name,
+ data: website,
+ },
+ }
+
+ rootItem.children!.push(website.name)
+ }
+
+ return rootItems
+ }, [websites])
+
+ return (
+
+ label="Websites"
+ initialItem={initialItem.name}
+ items={treeItems}
+ getItemTitle={(item) => item.data.label}
+ onPrimaryAction={(items) => {
+ if (items.data.data) {
+ onSelect(items.data.data)
+ }
+ }}
+ />
+ )
+}
+
+export default SiteTreeView
diff --git a/pkg/dashboard/frontend/src/pages/websites.astro b/pkg/dashboard/frontend/src/pages/websites.astro
new file mode 100644
index 000000000..0e71da34d
--- /dev/null
+++ b/pkg/dashboard/frontend/src/pages/websites.astro
@@ -0,0 +1,8 @@
+---
+import SiteExplorer from "@/components/websites/SiteExplorer";
+import Layout from "../layouts/Layout.astro";
+---
+
+
+
+
diff --git a/pkg/dashboard/frontend/src/types.ts b/pkg/dashboard/frontend/src/types.ts
index c3f3e8cfe..f5e43ca73 100644
--- a/pkg/dashboard/frontend/src/types.ts
+++ b/pkg/dashboard/frontend/src/types.ts
@@ -126,6 +126,7 @@ export interface WebSocketResponse {
sqlDatabases: SQLDatabase[]
httpProxies: HttpProxy[]
websockets: WebSocket[]
+ websites: Website[]
queues: Queue[]
policies: {
[name: string]: Policy
@@ -237,3 +238,8 @@ export interface LogEntry {
time: string
origin: string
}
+
+export interface Website {
+ name: string
+ url: string
+}
diff --git a/pkg/project/project.go b/pkg/project/project.go
index 7438a41b3..4782508b4 100644
--- a/pkg/project/project.go
+++ b/pkg/project/project.go
@@ -624,7 +624,7 @@ func (p *Project) RunWebsites(localCloud *cloud.LocalCloud) error {
sites = append(sites, websites.Website{
Name: site.Name,
WebsitePb: websites.WebsitePb{
- BasePath: site.outputPath,
+ BasePath: site.path,
OutputDirectory: outputDir,
IndexDocument: site.indexPage,
ErrorDocument: site.errorPage,
From e2787a3fbc673ab318d013627b573be2403fa9ea Mon Sep 17 00:00:00 2001
From: David Moore
Date: Wed, 12 Feb 2025 17:21:16 +1100
Subject: [PATCH 06/31] nitric start reverse proxy dev sites wip
---
cmd/start.go | 10 +++++++++
pkg/cloud/cloud.go | 2 +-
pkg/cloud/websites/websites.go | 41 +++++++++++++++++++++++++++++-----
pkg/project/config.go | 2 +-
pkg/project/project.go | 4 +++-
pkg/project/website.go | 3 +++
6 files changed, 54 insertions(+), 8 deletions(-)
diff --git a/cmd/start.go b/cmd/start.go
index 24097455b..2e7747f45 100644
--- a/cmd/start.go
+++ b/cmd/start.go
@@ -248,6 +248,16 @@ var startCmd = &cobra.Command{
}
}()
+ go func() {
+ err := proj.RunWebsites(localCloud)
+
+ if err != nil {
+ localCloud.Stop()
+
+ tui.CheckErr(err)
+ }
+ }()
+
// FIXME: This is a hack to get labelled logs into the TUI
// We should refactor the system logs to be more generic
systemChan := make(chan project.ServiceRunUpdate)
diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go
index 397fd3db4..a312d0c89 100644
--- a/pkg/cloud/cloud.go
+++ b/pkg/cloud/cloud.go
@@ -317,7 +317,7 @@ func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) {
return nil, err
}
- localWebsites := websites.NewLocalWebsitesService(localGateway.GetApiAddress)
+ localWebsites := websites.NewLocalWebsitesService(localGateway.GetApiAddress, opts.LocalCloudMode == LocalCloudModeStart)
return &LocalCloud{
servers: make(map[string]*server.NitricServer),
diff --git a/pkg/cloud/websites/websites.go b/pkg/cloud/websites/websites.go
index b83f9f33a..159acbabb 100644
--- a/pkg/cloud/websites/websites.go
+++ b/pkg/cloud/websites/websites.go
@@ -38,7 +38,8 @@ type WebsitePb = deploymentspb.Website
type Website struct {
WebsitePb
- Name string
+ Name string
+ DevURL string
}
type (
@@ -52,6 +53,7 @@ type LocalWebsiteService struct {
state State
port int
getApiAddress GetApiAddress
+ isStartCmd bool
bus EventBus.Bus
}
@@ -88,12 +90,40 @@ func (l *LocalWebsiteService) deregister(websiteName string) {
}
type staticSiteHandler struct {
- website *Website
- port int
+ website *Website
+ port int
+ devURL string
+ isStartCmd bool
}
// ServeHTTP - Serve a static website from the local filesystem
func (h staticSiteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ // if start command just proxy the request to the dev url
+ if h.isStartCmd {
+ // Target backend API server
+ target, err := url.Parse(h.devURL)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ // ignore proxy errors like unsupported protocol
+ if target == nil || target.Scheme == "" {
+ return
+ }
+
+ // Reverse proxy request
+ proxy := httputil.NewSingleHostReverseProxy(target)
+ proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadGateway)
+ }
+ }
+ proxy.ServeHTTP(w, r)
+
+ return
+ }
+
path := filepath.Join(h.website.OutputDirectory, r.URL.Path)
// check whether a file exists or is a directory at the given path
@@ -163,7 +193,7 @@ func (l *LocalWebsiteService) Start(websites []Website) error {
// Register the SPA handler for each website
for i := range websites {
website := &websites[i]
- spa := staticSiteHandler{website: website, port: l.port}
+ spa := staticSiteHandler{website: website, port: l.port, devURL: website.DevURL, isStartCmd: l.isStartCmd}
if website.BasePath == "/" {
mux.Handle("/", spa)
@@ -188,10 +218,11 @@ func (l *LocalWebsiteService) Start(websites []Website) error {
return nil
}
-func NewLocalWebsitesService(getApiAddress GetApiAddress) *LocalWebsiteService {
+func NewLocalWebsitesService(getApiAddress GetApiAddress, isStartCmd bool) *LocalWebsiteService {
return &LocalWebsiteService{
state: State{},
bus: EventBus.New(),
getApiAddress: getApiAddress,
+ isStartCmd: isStartCmd,
}
}
diff --git a/pkg/project/config.go b/pkg/project/config.go
index ad50ea26b..5f4e19e66 100644
--- a/pkg/project/config.go
+++ b/pkg/project/config.go
@@ -91,7 +91,7 @@ type Build struct {
type Dev struct {
Command string `yaml:"command"`
- Url string `yaml:"url,omitempty"`
+ URL string `yaml:"url,omitempty"`
}
type WebsiteConfiguration struct {
diff --git a/pkg/project/project.go b/pkg/project/project.go
index 4782508b4..9be7e526a 100644
--- a/pkg/project/project.go
+++ b/pkg/project/project.go
@@ -622,7 +622,8 @@ func (p *Project) RunWebsites(localCloud *cloud.LocalCloud) error {
}
sites = append(sites, websites.Website{
- Name: site.Name,
+ Name: site.Name,
+ DevURL: site.devURL,
WebsitePb: websites.WebsitePb{
BasePath: site.path,
OutputDirectory: outputDir,
@@ -827,6 +828,7 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *
outputPath: websiteSpec.Build.Output,
buildCmd: websiteSpec.Build.Command,
devCmd: websiteSpec.Dev.Command,
+ devURL: websiteSpec.Dev.URL,
indexPage: websiteSpec.IndexPage,
errorPage: websiteSpec.ErrorPage,
})
diff --git a/pkg/project/website.go b/pkg/project/website.go
index 281d7dc93..de547851f 100644
--- a/pkg/project/website.go
+++ b/pkg/project/website.go
@@ -40,6 +40,9 @@ type Website struct {
// the dev command to run the website
devCmd string
+ // the dev url for the website
+ devURL string
+
// the path to the website source files
outputPath string
From a2c757b761a0d64f5fcad1b68cb7cd96af5de71e Mon Sep 17 00:00:00 2001
From: David Moore
Date: Thu, 13 Feb 2025 17:15:07 +1100
Subject: [PATCH 07/31] add more validation and website dashboard info
---
pkg/cloud/cloud.go | 60 +++++-----
pkg/cloud/websites/websites.go | 25 ++++-
pkg/dashboard/dashboard.go | 18 ++-
.../src/components/websites/SiteExplorer.tsx | 105 +++++++++++++-----
pkg/dashboard/frontend/src/types.ts | 3 +
pkg/preview/feature.go | 1 +
pkg/project/project.go | 24 +++-
pkg/project/website.go | 4 +
pkg/view/tui/commands/local/run.go | 4 +-
9 files changed, 176 insertions(+), 68 deletions(-)
diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go
index a312d0c89..3d13a1a5f 100644
--- a/pkg/cloud/cloud.go
+++ b/pkg/cloud/cloud.go
@@ -53,9 +53,20 @@ type Subscribable[T any, A any] interface {
type ServiceName = string
+// LocalCloudMode type run or start
+type LocalCloudMode string
+
+const (
+ // LocalCloudModeRun - run mode
+ LocalCloudModeRun LocalCloudMode = "run"
+ // LocalCloudModeStart - start mode
+ LocalCloudModeStart LocalCloudMode = "start"
+)
+
type LocalCloud struct {
- serverLock sync.Mutex
- servers map[ServiceName]*server.NitricServer
+ serverLock sync.Mutex
+ servers map[ServiceName]*server.NitricServer
+ localCloudMode LocalCloudMode
Apis *apis.LocalApiGatewayService
Batch *batch.LocalBatchService
@@ -73,6 +84,10 @@ type LocalCloud struct {
Databases *sql.LocalSqlServer
}
+func (lc *LocalCloud) GetLocalCloudMode() LocalCloudMode {
+ return lc.localCloudMode
+}
+
// StartLocalNitric - starts the Nitric Server, including plugins and their local dependencies (e.g. local versions of cloud services)
func (lc *LocalCloud) Stop() {
for _, m := range lc.servers {
@@ -232,16 +247,6 @@ func (lc *LocalCloud) AddService(serviceName string) (int, error) {
return ports[0], nil
}
-// LocalCloudMode type run or start
-type LocalCloudMode string
-
-const (
- // LocalCloudModeRun - run mode
- LocalCloudModeRun LocalCloudMode = "run"
- // LocalCloudModeStart - start mode
- LocalCloudModeStart LocalCloudMode = "start"
-)
-
type LocalCloudOptions struct {
TLSCredentials *gateway.TLSCredentials
LogWriter io.Writer
@@ -320,20 +325,21 @@ func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) {
localWebsites := websites.NewLocalWebsitesService(localGateway.GetApiAddress, opts.LocalCloudMode == LocalCloudModeStart)
return &LocalCloud{
- servers: make(map[string]*server.NitricServer),
- Apis: localApis,
- Batch: localBatch,
- Http: localHttpProxy,
- Resources: localResources,
- Schedules: localSchedules,
- Storage: localStorage,
- Topics: localTopics,
- Websockets: localWebsockets,
- Websites: localWebsites,
- Gateway: localGateway,
- Secrets: localSecrets,
- KeyValue: keyvalueService,
- Queues: localQueueService,
- Databases: localDatabaseService,
+ servers: make(map[string]*server.NitricServer),
+ localCloudMode: opts.LocalCloudMode,
+ Apis: localApis,
+ Batch: localBatch,
+ Http: localHttpProxy,
+ Resources: localResources,
+ Schedules: localSchedules,
+ Storage: localStorage,
+ Topics: localTopics,
+ Websockets: localWebsockets,
+ Websites: localWebsites,
+ Gateway: localGateway,
+ Secrets: localSecrets,
+ KeyValue: keyvalueService,
+ Queues: localQueueService,
+ Databases: localDatabaseService,
}, nil
}
diff --git a/pkg/cloud/websites/websites.go b/pkg/cloud/websites/websites.go
index 159acbabb..a86fc4dae 100644
--- a/pkg/cloud/websites/websites.go
+++ b/pkg/cloud/websites/websites.go
@@ -36,15 +36,17 @@ import (
type WebsitePb = deploymentspb.Website
type Website struct {
- WebsitePb
+ *WebsitePb
- Name string
- DevURL string
+ Name string
+ Directory string
+ DevURL string
+ URL string
}
type (
WebsiteName = string
- State = map[WebsiteName]string
+ State = map[WebsiteName]Website
GetApiAddress = func(apiName string) string
)
@@ -74,7 +76,14 @@ func (l *LocalWebsiteService) register(website Website) {
l.websiteRegLock.Lock()
defer l.websiteRegLock.Unlock()
- l.state[website.Name] = fmt.Sprintf("http://localhost:%d/%s", l.port, strings.TrimPrefix(website.BasePath, "/"))
+ // add URL to the website and store it in the state
+ l.state[website.Name] = Website{
+ WebsitePb: website.WebsitePb,
+ Name: website.Name,
+ DevURL: website.DevURL,
+ Directory: website.Directory,
+ URL: fmt.Sprintf("http://localhost:%d/%s", l.port, strings.TrimPrefix(website.BasePath, "/")),
+ }
l.publishState()
}
@@ -100,6 +109,12 @@ type staticSiteHandler struct {
func (h staticSiteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// if start command just proxy the request to the dev url
if h.isStartCmd {
+ // if the dev url is not set, return a 500 internal server error with a message
+ if h.devURL == "" {
+ http.Error(w, "The dev URL is not set for this website", http.StatusInternalServerError)
+ return
+ }
+
// Target backend API server
target, err := url.Parse(h.devURL)
if err != nil {
diff --git a/pkg/dashboard/dashboard.go b/pkg/dashboard/dashboard.go
index 902c4e547..570197175 100644
--- a/pkg/dashboard/dashboard.go
+++ b/pkg/dashboard/dashboard.go
@@ -158,12 +158,15 @@ type HttpProxySpec struct {
}
type WebsiteSpec struct {
- Name string `json:"name"`
- URL string `json:"url"`
+ Name string `json:"name"`
+ URL string `json:"url"`
+ DevURL string `json:"devUrl"`
+ Directory string `json:"directory"`
}
type Dashboard struct {
resourcesLock sync.Mutex
+ localCloudMode cloud.LocalCloudMode
project *project.Project
storageService *storage.LocalStorageService
gatewayService *gateway.LocalGatewayService
@@ -229,6 +232,7 @@ type DashboardResponse struct {
CurrentVersion string `json:"currentVersion"`
LatestVersion string `json:"latestVersion"`
Connected bool `json:"connected"`
+ LocalCloudMode cloud.LocalCloudMode `json:"localCloudMode"`
}
type Bucket struct {
@@ -644,10 +648,12 @@ func (d *Dashboard) handleWebsites(state websites.State) {
websites := []WebsiteSpec{}
- for name, url := range state {
+ for name, site := range state {
websites = append(websites, WebsiteSpec{
- Name: strings.TrimPrefix(name, "websites_"),
- URL: url,
+ Name: strings.TrimPrefix(name, "websites_"),
+ URL: site.URL,
+ DevURL: site.DevURL,
+ Directory: site.Directory,
})
}
@@ -869,6 +875,7 @@ func (d *Dashboard) sendStackUpdate() error {
CurrentVersion: currentVersion,
LatestVersion: latestVersion,
Connected: d.isConnected(),
+ LocalCloudMode: d.localCloudMode,
}
// Encode the response as JSON
@@ -921,6 +928,7 @@ func New(noBrowser bool, localCloud *cloud.LocalCloud, project *project.Project)
wsWebSocket := melody.New()
dash := &Dashboard{
+ localCloudMode: localCloud.GetLocalCloudMode(),
project: project,
storageService: localCloud.Storage,
gatewayService: localCloud.Gateway,
diff --git a/pkg/dashboard/frontend/src/components/websites/SiteExplorer.tsx b/pkg/dashboard/frontend/src/components/websites/SiteExplorer.tsx
index d34a04350..467830bc8 100644
--- a/pkg/dashboard/frontend/src/components/websites/SiteExplorer.tsx
+++ b/pkg/dashboard/frontend/src/components/websites/SiteExplorer.tsx
@@ -16,14 +16,22 @@ import {
import NotFoundAlert from '../shared/NotFoundAlert'
import SiteTreeView from './SiteTreeView'
import { Button } from '../ui/button'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '../ui/dropdown-menu'
+import { EllipsisVerticalIcon } from '@heroicons/react/24/outline'
-const LOCAL_STORAGE_KEY = 'nitric-local-dash-storage-history'
+const LOCAL_STORAGE_KEY = 'nitric-local-dash-website-history'
const SiteExplorer = () => {
const [selectedWebsite, setSelectedWebsite] = useState()
const { data, loading } = useWebSocket()
- const { websites } = data || {}
+ const { websites, localCloudMode } = data || {}
useEffect(() => {
if (websites?.length && !selectedWebsite) {
@@ -82,15 +90,25 @@ const SiteExplorer = () => {
{selectedWebsite.name}
-
-
- Open in a new tab
-
-
+
{!data?.websites.some(
@@ -123,15 +141,40 @@ const SiteExplorer = () => {
-
-
- Open in a new tab
-
-
+
+
+
+ Open website actions
+
+
+
+
+
+
+
+ Open in VSCode
+
+
+
+
+ Open in a new tab
+
+
+
+
+
@@ -139,11 +182,23 @@ const SiteExplorer = () => {
{selectedWebsite.url}
-
+ {localCloudMode === 'run' || selectedWebsite.devUrl ? (
+
+ ) : (
+
+ A development URL is required when running{' '}
+
+ nitric start
+ {' '}
+ to ensure the website operates in development mode.
+ Set the URL in your website dev config within your
+ nitric.yaml file, e.g. http://localhost:4321.
+
+ )}
diff --git a/pkg/dashboard/frontend/src/types.ts b/pkg/dashboard/frontend/src/types.ts
index f5e43ca73..5ddb2fb16 100644
--- a/pkg/dashboard/frontend/src/types.ts
+++ b/pkg/dashboard/frontend/src/types.ts
@@ -139,6 +139,7 @@ export interface WebSocketResponse {
currentVersion: string
latestVersion: string
connected: boolean
+ localCloudMode: 'start' | 'run'
}
export interface Param {
@@ -242,4 +243,6 @@ export interface LogEntry {
export interface Website {
name: string
url: string
+ devUrl: string
+ directory: string
}
diff --git a/pkg/preview/feature.go b/pkg/preview/feature.go
index ef6a3da82..032df7a49 100644
--- a/pkg/preview/feature.go
+++ b/pkg/preview/feature.go
@@ -23,4 +23,5 @@ const (
Feature_BetaProviders Feature = "beta-providers"
Feature_SqlDatabases Feature = "sql-databases"
Feature_BatchServices Feature = "batch-services"
+ Feature_Websites Feature = "websites"
)
diff --git a/pkg/project/project.go b/pkg/project/project.go
index 9be7e526a..89509c570 100644
--- a/pkg/project/project.go
+++ b/pkg/project/project.go
@@ -621,10 +621,16 @@ func (p *Project) RunWebsites(localCloud *cloud.LocalCloud) error {
return fmt.Errorf("unable to get absolute output path for website %s: %w", site.basedir, err)
}
+ directory, err := site.GetAbsoluteDirectory()
+ if err != nil {
+ return fmt.Errorf("unable to get absolute directory for website %s: %w", site.basedir, err)
+ }
+
sites = append(sites, websites.Website{
- Name: site.Name,
- DevURL: site.devURL,
- WebsitePb: websites.WebsitePb{
+ Name: site.Name,
+ DevURL: site.devURL,
+ Directory: directory,
+ WebsitePb: &websites.WebsitePb{
BasePath: site.path,
OutputDirectory: outputDir,
IndexDocument: site.indexPage,
@@ -804,17 +810,23 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *
return nil, fmt.Errorf("no build output provided for website %s", websiteSpec.GetBasedir())
}
- // apply defaults
+ // apply defaults and validate website configuration
if websiteSpec.Path == "" {
websiteSpec.Path = "/"
+ } else if !strings.HasPrefix(websiteSpec.Path, "/") {
+ return nil, fmt.Errorf("invalid website path %s, must start with a /", websiteSpec.Path)
}
if websiteSpec.IndexPage == "" {
websiteSpec.IndexPage = "index.html"
+ } else if !strings.HasSuffix(websiteSpec.IndexPage, ".html") {
+ return nil, fmt.Errorf("invalid index page %s, must end with .html", websiteSpec.IndexPage)
}
if websiteSpec.ErrorPage == "" {
websiteSpec.ErrorPage = "index.html"
+ } else if !strings.HasSuffix(websiteSpec.ErrorPage, ".html") {
+ return nil, fmt.Errorf("invalid error page %s, must end with .html", websiteSpec.ErrorPage)
}
projectRelativeWebsiteFolder := filepath.Join(projectConfig.Directory, websiteSpec.GetBasedir())
@@ -847,6 +859,10 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *
return nil, fmt.Errorf("duplicate website paths found: %s", strings.Join(duplicatePaths, ", "))
}
+ if len(websites) > 0 && !slices.Contains(projectConfig.Preview, preview.Feature_Websites) {
+ return nil, fmt.Errorf("project contains websites, but the project does not have the 'websites' preview feature enabled. Please add websites to the preview field of your nitric.yaml file to enable this feature")
+ }
+
// create an empty local configuration if none is provided
if localConfig == nil {
localConfig = &localconfig.LocalConfiguration{}
diff --git a/pkg/project/website.go b/pkg/project/website.go
index de547851f..ad42b9f0e 100644
--- a/pkg/project/website.go
+++ b/pkg/project/website.go
@@ -61,6 +61,10 @@ func (s *Website) GetAbsoluteOutputPath() (string, error) {
return filepath.Abs(s.GetOutputPath())
}
+func (s *Website) GetAbsoluteDirectory() (string, error) {
+ return filepath.Abs(s.basedir)
+}
+
// Run - runs the website using the provided dev command
func (s *Website) Run(stop <-chan bool, updates chan<- ServiceRunUpdate, env map[string]string) error {
if s.devCmd == "" {
diff --git a/pkg/view/tui/commands/local/run.go b/pkg/view/tui/commands/local/run.go
index 7aac736a1..12963e89f 100644
--- a/pkg/view/tui/commands/local/run.go
+++ b/pkg/view/tui/commands/local/run.go
@@ -234,10 +234,10 @@ func (t *TuiModel) ReactiveUpdate(msg tea.Msg) (tea.Model, tea.Cmd) {
case websites.State:
newWebsitesSummary := []WebsiteSummary{}
- for websiteName, url := range state {
+ for websiteName, site := range state {
newWebsitesSummary = append(newWebsitesSummary, WebsiteSummary{
name: strings.TrimPrefix(websiteName, "websites_"),
- url: url,
+ url: site.URL,
})
}
From c35f4421aa333c6fa45d935226b1f46dc0746311 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Thu, 13 Feb 2025 17:15:19 +1100
Subject: [PATCH 08/31] remove redundant ports config
---
pkg/project/config.go | 1 -
1 file changed, 1 deletion(-)
diff --git a/pkg/project/config.go b/pkg/project/config.go
index 5f4e19e66..cb8e8b777 100644
--- a/pkg/project/config.go
+++ b/pkg/project/config.go
@@ -108,7 +108,6 @@ type ProjectConfiguration struct {
Name string `yaml:"name"`
Directory string `yaml:"-"`
Services []ServiceConfiguration `yaml:"services"`
- Ports map[string]int `yaml:"ports,omitempty"`
Batches []BatchConfiguration `yaml:"batch-services"`
Websites []WebsiteConfiguration `yaml:"websites"`
Runtimes map[string]RuntimeConfiguration `yaml:"runtimes,omitempty"`
From 8d22c05356bb251215082d0d19ec008b9d256009 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Fri, 14 Feb 2025 12:31:19 +1100
Subject: [PATCH 09/31] add websites to arch diagram
---
.../architecture/nodes/WebsitesNode.tsx | 42 +++++++++++++++++
.../src/components/architecture/styles.css | 7 +++
.../src/components/websites/SitesList.tsx | 46 +++++++++++++++++++
.../lib/utils/generate-architecture-data.ts | 42 +++++++++++++++++
4 files changed, 137 insertions(+)
create mode 100644 pkg/dashboard/frontend/src/components/architecture/nodes/WebsitesNode.tsx
create mode 100644 pkg/dashboard/frontend/src/components/websites/SitesList.tsx
diff --git a/pkg/dashboard/frontend/src/components/architecture/nodes/WebsitesNode.tsx b/pkg/dashboard/frontend/src/components/architecture/nodes/WebsitesNode.tsx
new file mode 100644
index 000000000..f45e1479b
--- /dev/null
+++ b/pkg/dashboard/frontend/src/components/architecture/nodes/WebsitesNode.tsx
@@ -0,0 +1,42 @@
+import { type ComponentType } from 'react'
+
+import type { Website } from '@/types'
+import type { NodeProps } from 'reactflow'
+import NodeBase, { type NodeBaseData } from './NodeBase'
+import SitesList from '@/components/websites/SitesList'
+
+export type WebsitesNodeData = NodeBaseData
+
+export const WebsitesNode: ComponentType> = (
+ props,
+) => {
+ const { data } = props
+
+ const websites = data.resource
+
+ const rootWebsite = websites.find((website) =>
+ /localhost:\d+$/.test(website.url.replace(/\/$/, '')),
+ )
+
+ const description = `${websites.length} websites stored in a bucket and served via CDN.`
+
+ return (
+ site !== rootWebsite)}
+ />
+ ) : null,
+ }}
+ />
+ )
+}
diff --git a/pkg/dashboard/frontend/src/components/architecture/styles.css b/pkg/dashboard/frontend/src/components/architecture/styles.css
index 94738691a..179daa904 100644
--- a/pkg/dashboard/frontend/src/components/architecture/styles.css
+++ b/pkg/dashboard/frontend/src/components/architecture/styles.css
@@ -139,3 +139,10 @@
--nitric-node-to: #334155; /* Slate 700 */
--nitric-node-icon-color: #475569; /* Slate 600 */
}
+
+.react-flow__node-websites {
+ --nitric-node-from: #06b6d4; /* Cyan 600 */
+ --nitric-node-via: #22d3ee; /* Cyan 400 */
+ --nitric-node-to: #0891b2; /* Cyan 700 */
+ --nitric-node-icon-color: #06b6d4; /* Cyan 600 */
+}
diff --git a/pkg/dashboard/frontend/src/components/websites/SitesList.tsx b/pkg/dashboard/frontend/src/components/websites/SitesList.tsx
new file mode 100644
index 000000000..5900c44a8
--- /dev/null
+++ b/pkg/dashboard/frontend/src/components/websites/SitesList.tsx
@@ -0,0 +1,46 @@
+import React from 'react'
+import type { Website } from '@/types'
+
+interface SitesListProps {
+ subsites: Website[]
+ rootSite: Website
+}
+
+const SitesList: React.FC = ({ rootSite, subsites }) => {
+ return (
+
+
Root Site:
+
+
Subsites:
+
+ {subsites.map((website) => (
+
+ ))}
+
+
+ )
+}
+
+export default SitesList
diff --git a/pkg/dashboard/frontend/src/lib/utils/generate-architecture-data.ts b/pkg/dashboard/frontend/src/lib/utils/generate-architecture-data.ts
index e91a50b53..51336ce7c 100644
--- a/pkg/dashboard/frontend/src/lib/utils/generate-architecture-data.ts
+++ b/pkg/dashboard/frontend/src/lib/utils/generate-architecture-data.ts
@@ -19,6 +19,7 @@ import {
QueueListIcon,
LockClosedIcon,
CogIcon,
+ WindowIcon,
} from '@heroicons/react/24/outline'
import {
MarkerType,
@@ -64,6 +65,10 @@ import {
} from '@/components/architecture/nodes/BatchNode'
import { PERMISSION_TO_SDK_LABELS } from '../constants'
import { flattenPaths } from './flatten-paths'
+import {
+ WebsitesNode,
+ type WebsitesNodeData,
+} from '@/components/architecture/nodes/WebsitesNode'
export const nodeTypes = {
api: APINode,
@@ -79,6 +84,7 @@ export const nodeTypes = {
httpproxy: HttpProxyNode,
queue: QueueNode,
secret: SecretNode,
+ websites: WebsitesNode,
}
const createNode = (
@@ -569,6 +575,42 @@ export function generateArchitectureData(data: WebSocketResponse): {
}),
)
+ if (data.websites.length > 0) {
+ const websitesNode = createNode(
+ {
+ name: 'websites',
+ filePath: '',
+ requestingServices: [],
+ },
+ 'websites',
+ {
+ title: 'Websites',
+ resource: data.websites,
+ icon: WindowIcon,
+ },
+ )
+
+ nodes.push(websitesNode)
+
+ // create edges from websites to apis
+ data.apis.forEach((api) => {
+ edges.push({
+ id: `e-${api.name}-websites`,
+ source: websitesNode.id,
+ target: `api-${api.name}`,
+ animated: true,
+ markerEnd: {
+ type: MarkerType.ArrowClosed,
+ },
+ markerStart: {
+ type: MarkerType.ArrowClosed,
+ orient: 'auto-start-reverse',
+ },
+ label: `Rewrites to /api/${api.name}`,
+ })
+ })
+ }
+
data.services.forEach((service) => {
const node: Node = {
id: service.name,
From 1d32fbbbefded45e75aa13aff85d9257048fcca6 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Fri, 14 Feb 2025 16:09:50 +1100
Subject: [PATCH 10/31] fmt
---
cmd/start.go | 1 -
pkg/cloud/websites/websites.go | 13 +++----------
pkg/project/project.go | 2 --
pkg/project/website.go | 1 -
4 files changed, 3 insertions(+), 14 deletions(-)
diff --git a/cmd/start.go b/cmd/start.go
index 2e7747f45..e5c7e9742 100644
--- a/cmd/start.go
+++ b/cmd/start.go
@@ -250,7 +250,6 @@ var startCmd = &cobra.Command{
go func() {
err := proj.RunWebsites(localCloud)
-
if err != nil {
localCloud.Stop()
diff --git a/pkg/cloud/websites/websites.go b/pkg/cloud/websites/websites.go
index a86fc4dae..0290f58d7 100644
--- a/pkg/cloud/websites/websites.go
+++ b/pkg/cloud/websites/websites.go
@@ -29,6 +29,7 @@ import (
"sync"
"github.com/asaskevich/EventBus"
+
"github.com/nitrictech/cli/pkg/netx"
deploymentspb "github.com/nitrictech/nitric/core/pkg/proto/deployments/v1"
)
@@ -88,16 +89,6 @@ func (l *LocalWebsiteService) register(website Website) {
l.publishState()
}
-// deregister - Deregister a website
-func (l *LocalWebsiteService) deregister(websiteName string) {
- l.websiteRegLock.Lock()
- defer l.websiteRegLock.Unlock()
-
- delete(l.state, websiteName)
-
- l.publishState()
-}
-
type staticSiteHandler struct {
website *Website
port int
@@ -153,11 +144,13 @@ func (h staticSiteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// if we got an error (that wasn't that the file doesn't exist) stating the
// file, return a 500 internal server error and stop
http.Error(w, err.Error(), http.StatusInternalServerError)
+
return
}
if fi.IsDir() {
http.ServeFile(w, r, filepath.Join(h.website.OutputDirectory, h.website.IndexDocument))
+
return
}
diff --git a/pkg/project/project.go b/pkg/project/project.go
index 89509c570..81025bfba 100644
--- a/pkg/project/project.go
+++ b/pkg/project/project.go
@@ -209,7 +209,6 @@ func (p *Project) BuildWebsites(env map[string]string) (chan ServiceBuildUpdate,
Message: err.Error(),
Status: ServiceBuildStatus_Error,
}
-
} else {
updatesChan <- ServiceBuildUpdate{
ServiceName: site.Name,
@@ -467,7 +466,6 @@ func (p *Project) CollectWebsiteRequirements() ([]*deploymentpb.Website, error)
IndexDocument: site.indexPage,
ErrorDocument: site.errorPage,
})
-
}
return allWebsiteRequirements, nil
diff --git a/pkg/project/website.go b/pkg/project/website.go
index ad42b9f0e..220663e0c 100644
--- a/pkg/project/website.go
+++ b/pkg/project/website.go
@@ -203,7 +203,6 @@ func (s *Website) Build(updates chan ServiceBuildUpdate, env map[string]string)
}()
err := <-errChan
-
if err != nil {
updates <- ServiceBuildUpdate{
ServiceName: s.Name,
From fa048ea2d8b47c53bec568a7bdd5e0833e8b76bf Mon Sep 17 00:00:00 2001
From: David Moore
Date: Fri, 14 Feb 2025 16:23:23 +1100
Subject: [PATCH 11/31] cleanup of websites arch node
---
.../architecture/nodes/WebsitesNode.tsx | 2 +-
.../src/components/websites/SitesList.tsx | 37 +++++++++++--------
2 files changed, 23 insertions(+), 16 deletions(-)
diff --git a/pkg/dashboard/frontend/src/components/architecture/nodes/WebsitesNode.tsx b/pkg/dashboard/frontend/src/components/architecture/nodes/WebsitesNode.tsx
index f45e1479b..1965af439 100644
--- a/pkg/dashboard/frontend/src/components/architecture/nodes/WebsitesNode.tsx
+++ b/pkg/dashboard/frontend/src/components/architecture/nodes/WebsitesNode.tsx
@@ -18,7 +18,7 @@ export const WebsitesNode: ComponentType> = (
/localhost:\d+$/.test(website.url.replace(/\/$/, '')),
)
- const description = `${websites.length} websites stored in a bucket and served via CDN.`
+ const description = `${websites.length === 1 ? 'website' : 'websites'} stored in a bucket and served via CDN.`
return (
= ({ rootSite, subsites }) => {
- Subsites:
-
- {subsites.map((website) => (
-
+ >
+ ) : null}
)
}
From be800339523ea43ee8654dc349813132f07e7e27 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Fri, 14 Feb 2025 17:08:42 +1100
Subject: [PATCH 12/31] add website tests
---
pkg/dashboard/frontend/cypress/e2e/a11y.cy.ts | 1 +
.../frontend/cypress/e2e/architecture.cy.ts | 1 +
.../frontend/cypress/e2e/websites.cy.ts | 47 +++
.../lib/utils/generate-architecture-data.ts | 2 +-
.../frontend/test-app/docs-website/.gitignore | 24 ++
.../frontend/test-app/docs-website/index.html | 13 +
.../test-app/docs-website/package.json | 14 +
.../test-app/docs-website/public/vite.svg | 1 +
.../test-app/docs-website/src/counter.js | 9 +
.../test-app/docs-website/src/javascript.svg | 1 +
.../test-app/docs-website/src/main.js | 24 ++
.../test-app/docs-website/src/style.css | 96 +++++
.../frontend/test-app/docs-website/yarn.lock | 327 ++++++++++++++++++
pkg/dashboard/frontend/test-app/nitric.yaml | 18 +-
.../frontend/test-app/vite-website/.gitignore | 24 ++
.../frontend/test-app/vite-website/index.html | 13 +
.../test-app/vite-website/package.json | 14 +
.../test-app/vite-website/public/vite.svg | 1 +
.../test-app/vite-website/src/counter.js | 9 +
.../test-app/vite-website/src/javascript.svg | 1 +
.../test-app/vite-website/src/main.js | 24 ++
.../test-app/vite-website/src/style.css | 96 +++++
.../frontend/test-app/vite-website/yarn.lock | 327 ++++++++++++++++++
23 files changed, 1085 insertions(+), 2 deletions(-)
create mode 100644 pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
create mode 100644 pkg/dashboard/frontend/test-app/docs-website/.gitignore
create mode 100644 pkg/dashboard/frontend/test-app/docs-website/index.html
create mode 100644 pkg/dashboard/frontend/test-app/docs-website/package.json
create mode 100644 pkg/dashboard/frontend/test-app/docs-website/public/vite.svg
create mode 100644 pkg/dashboard/frontend/test-app/docs-website/src/counter.js
create mode 100644 pkg/dashboard/frontend/test-app/docs-website/src/javascript.svg
create mode 100644 pkg/dashboard/frontend/test-app/docs-website/src/main.js
create mode 100644 pkg/dashboard/frontend/test-app/docs-website/src/style.css
create mode 100644 pkg/dashboard/frontend/test-app/docs-website/yarn.lock
create mode 100644 pkg/dashboard/frontend/test-app/vite-website/.gitignore
create mode 100644 pkg/dashboard/frontend/test-app/vite-website/index.html
create mode 100644 pkg/dashboard/frontend/test-app/vite-website/package.json
create mode 100644 pkg/dashboard/frontend/test-app/vite-website/public/vite.svg
create mode 100644 pkg/dashboard/frontend/test-app/vite-website/src/counter.js
create mode 100644 pkg/dashboard/frontend/test-app/vite-website/src/javascript.svg
create mode 100644 pkg/dashboard/frontend/test-app/vite-website/src/main.js
create mode 100644 pkg/dashboard/frontend/test-app/vite-website/src/style.css
create mode 100644 pkg/dashboard/frontend/test-app/vite-website/yarn.lock
diff --git a/pkg/dashboard/frontend/cypress/e2e/a11y.cy.ts b/pkg/dashboard/frontend/cypress/e2e/a11y.cy.ts
index 2a67282e7..d768988c7 100644
--- a/pkg/dashboard/frontend/cypress/e2e/a11y.cy.ts
+++ b/pkg/dashboard/frontend/cypress/e2e/a11y.cy.ts
@@ -9,6 +9,7 @@ describe('a11y test suite', () => {
'/topics',
'/jobs',
'/websockets',
+ '/websites',
'/logs',
'/not-found',
]
diff --git a/pkg/dashboard/frontend/cypress/e2e/architecture.cy.ts b/pkg/dashboard/frontend/cypress/e2e/architecture.cy.ts
index 4ab7c63be..6e5064584 100644
--- a/pkg/dashboard/frontend/cypress/e2e/architecture.cy.ts
+++ b/pkg/dashboard/frontend/cypress/e2e/architecture.cy.ts
@@ -19,6 +19,7 @@ const expectedNodes = [
'services/my-test-secret.ts',
'my-first-secret',
'my-second-secret',
+ 'websites',
]
describe('Architecture Spec', () => {
diff --git a/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts b/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
new file mode 100644
index 000000000..e903d9427
--- /dev/null
+++ b/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
@@ -0,0 +1,47 @@
+describe('Websites Spec', () => {
+ beforeEach(() => {
+ cy.viewport('macbook-16')
+ })
+
+ const expectedWebsites = ['vite-website', 'docs-website']
+
+ it('should retrieve correct websites in list', () => {
+ cy.visit('/websites')
+ cy.get('h2').should('contain.text', 'vite-website')
+
+ expectedWebsites.forEach((id) => {
+ cy.get(`[data-rct-item-id="${id}"]`).should('exist')
+ })
+ })
+
+ expectedWebsites.forEach((id) => {
+ it(`should render website ${id}`, () => {
+ cy.visit('/websites')
+ cy.get(`[data-rct-item-id="${id}"]`).click()
+ cy.get('h2').should('contain.text', id)
+
+ const pathMap = {
+ 'vite-website': '',
+ 'docs-website': 'docs',
+ }
+
+ const url = `http://localhost:5000/${pathMap[id]}`
+
+ // check iframe url
+ cy.get('iframe').should('have.attr', 'src', url)
+
+ cy.visit(url)
+
+ const titleMap = {
+ 'vite-website': 'Hello Nitric!',
+ 'docs-website': 'Hello Nitric Docs Test!',
+ }
+
+ const title = titleMap[id]
+
+ cy.origin(url, { args: { title } }, ({ title }) => {
+ cy.get('h1').should('have.text', title)
+ })
+ })
+ })
+})
diff --git a/pkg/dashboard/frontend/src/lib/utils/generate-architecture-data.ts b/pkg/dashboard/frontend/src/lib/utils/generate-architecture-data.ts
index 51336ce7c..e26e79a3b 100644
--- a/pkg/dashboard/frontend/src/lib/utils/generate-architecture-data.ts
+++ b/pkg/dashboard/frontend/src/lib/utils/generate-architecture-data.ts
@@ -584,7 +584,7 @@ export function generateArchitectureData(data: WebSocketResponse): {
},
'websites',
{
- title: 'Websites',
+ title: 'websites',
resource: data.websites,
icon: WindowIcon,
},
diff --git a/pkg/dashboard/frontend/test-app/docs-website/.gitignore b/pkg/dashboard/frontend/test-app/docs-website/.gitignore
new file mode 100644
index 000000000..a547bf36d
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/docs-website/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/pkg/dashboard/frontend/test-app/docs-website/index.html b/pkg/dashboard/frontend/test-app/docs-website/index.html
new file mode 100644
index 000000000..72ba3a8b3
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/docs-website/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/pkg/dashboard/frontend/test-app/docs-website/package.json b/pkg/dashboard/frontend/test-app/docs-website/package.json
new file mode 100644
index 000000000..0beeed35f
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/docs-website/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "docs-website",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "vite": "^6.1.0"
+ }
+}
diff --git a/pkg/dashboard/frontend/test-app/docs-website/public/vite.svg b/pkg/dashboard/frontend/test-app/docs-website/public/vite.svg
new file mode 100644
index 000000000..e7b8dfb1b
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/docs-website/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pkg/dashboard/frontend/test-app/docs-website/src/counter.js b/pkg/dashboard/frontend/test-app/docs-website/src/counter.js
new file mode 100644
index 000000000..881e2d7ad
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/docs-website/src/counter.js
@@ -0,0 +1,9 @@
+export function setupCounter(element) {
+ let counter = 0
+ const setCounter = (count) => {
+ counter = count
+ element.innerHTML = `count is ${counter}`
+ }
+ element.addEventListener('click', () => setCounter(counter + 1))
+ setCounter(0)
+}
diff --git a/pkg/dashboard/frontend/test-app/docs-website/src/javascript.svg b/pkg/dashboard/frontend/test-app/docs-website/src/javascript.svg
new file mode 100644
index 000000000..f9abb2b72
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/docs-website/src/javascript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pkg/dashboard/frontend/test-app/docs-website/src/main.js b/pkg/dashboard/frontend/test-app/docs-website/src/main.js
new file mode 100644
index 000000000..e96b5f712
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/docs-website/src/main.js
@@ -0,0 +1,24 @@
+import './style.css'
+import javascriptLogo from './javascript.svg'
+import viteLogo from '/vite.svg'
+import { setupCounter } from './counter.js'
+
+document.querySelector('#app').innerHTML = `
+
+
+
+
+
+
+
+
Hello Nitric Docs Test!
+
+
+
+
+ Click on the Vite logo to learn more
+
+
+`
+
+setupCounter(document.querySelector('#counter'))
diff --git a/pkg/dashboard/frontend/test-app/docs-website/src/style.css b/pkg/dashboard/frontend/test-app/docs-website/src/style.css
new file mode 100644
index 000000000..30aa81410
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/docs-website/src/style.css
@@ -0,0 +1,96 @@
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+#app {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.vanilla:hover {
+ filter: drop-shadow(0 0 2em #f7df1eaa);
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
diff --git a/pkg/dashboard/frontend/test-app/docs-website/yarn.lock b/pkg/dashboard/frontend/test-app/docs-website/yarn.lock
new file mode 100644
index 000000000..15c78083c
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/docs-website/yarn.lock
@@ -0,0 +1,327 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@esbuild/aix-ppc64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461"
+ integrity sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==
+
+"@esbuild/android-arm64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz#f592957ae8b5643129fa889c79e69cd8669bb894"
+ integrity sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==
+
+"@esbuild/android-arm@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz#72d8a2063aa630308af486a7e5cbcd1e134335b3"
+ integrity sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==
+
+"@esbuild/android-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz#9a7713504d5f04792f33be9c197a882b2d88febb"
+ integrity sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==
+
+"@esbuild/darwin-arm64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz#02ae04ad8ebffd6e2ea096181b3366816b2b5936"
+ integrity sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==
+
+"@esbuild/darwin-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz#9ec312bc29c60e1b6cecadc82bd504d8adaa19e9"
+ integrity sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==
+
+"@esbuild/freebsd-arm64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz#5e82f44cb4906d6aebf24497d6a068cfc152fa00"
+ integrity sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==
+
+"@esbuild/freebsd-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz#3fb1ce92f276168b75074b4e51aa0d8141ecce7f"
+ integrity sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==
+
+"@esbuild/linux-arm64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz#856b632d79eb80aec0864381efd29de8fd0b1f43"
+ integrity sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==
+
+"@esbuild/linux-arm@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz#c846b4694dc5a75d1444f52257ccc5659021b736"
+ integrity sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==
+
+"@esbuild/linux-ia32@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz#f8a16615a78826ccbb6566fab9a9606cfd4a37d5"
+ integrity sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==
+
+"@esbuild/linux-loong64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz#1c451538c765bf14913512c76ed8a351e18b09fc"
+ integrity sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==
+
+"@esbuild/linux-mips64el@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz#0846edeefbc3d8d50645c51869cc64401d9239cb"
+ integrity sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==
+
+"@esbuild/linux-ppc64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz#8e3fc54505671d193337a36dfd4c1a23b8a41412"
+ integrity sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==
+
+"@esbuild/linux-riscv64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz#6a1e92096d5e68f7bb10a0d64bb5b6d1daf9a694"
+ integrity sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==
+
+"@esbuild/linux-s390x@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz#ab18e56e66f7a3c49cb97d337cd0a6fea28a8577"
+ integrity sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==
+
+"@esbuild/linux-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz#8140c9b40da634d380b0b29c837a0b4267aff38f"
+ integrity sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==
+
+"@esbuild/netbsd-arm64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz#65f19161432bafb3981f5f20a7ff45abb2e708e6"
+ integrity sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==
+
+"@esbuild/netbsd-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz#7a3a97d77abfd11765a72f1c6f9b18f5396bcc40"
+ integrity sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==
+
+"@esbuild/openbsd-arm64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz#58b00238dd8f123bfff68d3acc53a6ee369af89f"
+ integrity sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==
+
+"@esbuild/openbsd-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz#0ac843fda0feb85a93e288842936c21a00a8a205"
+ integrity sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==
+
+"@esbuild/sunos-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz#8b7aa895e07828d36c422a4404cc2ecf27fb15c6"
+ integrity sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==
+
+"@esbuild/win32-arm64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz#c023afb647cabf0c3ed13f0eddfc4f1d61c66a85"
+ integrity sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==
+
+"@esbuild/win32-ia32@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz#96c356132d2dda990098c8b8b951209c3cd743c2"
+ integrity sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==
+
+"@esbuild/win32-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b"
+ integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==
+
+"@rollup/rollup-android-arm-eabi@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.6.tgz#9b726b4dcafb9332991e9ca49d54bafc71d9d87f"
+ integrity sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==
+
+"@rollup/rollup-android-arm64@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.6.tgz#88326ff46168a47851077ca0bf0c442689ec088f"
+ integrity sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==
+
+"@rollup/rollup-darwin-arm64@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.6.tgz#b8fbcc9389bc6fad3334a1d16dbeaaa5637c5772"
+ integrity sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==
+
+"@rollup/rollup-darwin-x64@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.6.tgz#1aa2bcad84c0fb5902e945d88822e17a4f661d51"
+ integrity sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==
+
+"@rollup/rollup-freebsd-arm64@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.6.tgz#29c54617e0929264dcb6416597d6d7481696e49f"
+ integrity sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==
+
+"@rollup/rollup-freebsd-x64@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.6.tgz#a8b58ab7d31882559d93f2d1b5863d9e4b4b2678"
+ integrity sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.6.tgz#a844e1978c8b9766b169ecb1cb5cc0d8a3f05930"
+ integrity sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==
+
+"@rollup/rollup-linux-arm-musleabihf@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.6.tgz#6b44c3b7257985d71b087fcb4ef01325e2fff201"
+ integrity sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==
+
+"@rollup/rollup-linux-arm64-gnu@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.6.tgz#ebb499cf1720115256d0c9ae7598c90cc2251bc5"
+ integrity sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==
+
+"@rollup/rollup-linux-arm64-musl@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.6.tgz#9658221b59d9e5643348f9a52fa5ef35b4dc07b1"
+ integrity sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==
+
+"@rollup/rollup-linux-loongarch64-gnu@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.6.tgz#19418cc57579a5655af2d850a89d74b3f7e9aa92"
+ integrity sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==
+
+"@rollup/rollup-linux-powerpc64le-gnu@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.6.tgz#fe0bce7778cb6ce86898c781f3f11369d1a4952c"
+ integrity sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==
+
+"@rollup/rollup-linux-riscv64-gnu@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.6.tgz#9c158360abf6e6f7794285642ba0898c580291f6"
+ integrity sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==
+
+"@rollup/rollup-linux-s390x-gnu@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.6.tgz#f9113498d22962baacdda008b5587d568b05aa34"
+ integrity sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==
+
+"@rollup/rollup-linux-x64-gnu@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.6.tgz#aec8d4cdf911cd869a72b8bd00833cb426664e0c"
+ integrity sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==
+
+"@rollup/rollup-linux-x64-musl@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.6.tgz#61c0a146bdd1b5e0dcda33690dd909b321d8f20f"
+ integrity sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==
+
+"@rollup/rollup-win32-arm64-msvc@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.6.tgz#c6c5bf290a3a459c18871110bc2e7009ce35b15a"
+ integrity sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==
+
+"@rollup/rollup-win32-ia32-msvc@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.6.tgz#16ca6bdadc9e054818b9c51f8dac82f6b8afab81"
+ integrity sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==
+
+"@rollup/rollup-win32-x64-msvc@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.6.tgz#f3d03ce2d82723eb089188ea1494a719b09e1561"
+ integrity sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==
+
+"@types/estree@1.0.6":
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
+ integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
+
+esbuild@^0.24.2:
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.2.tgz#b5b55bee7de017bff5fb8a4e3e44f2ebe2c3567d"
+ integrity sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==
+ optionalDependencies:
+ "@esbuild/aix-ppc64" "0.24.2"
+ "@esbuild/android-arm" "0.24.2"
+ "@esbuild/android-arm64" "0.24.2"
+ "@esbuild/android-x64" "0.24.2"
+ "@esbuild/darwin-arm64" "0.24.2"
+ "@esbuild/darwin-x64" "0.24.2"
+ "@esbuild/freebsd-arm64" "0.24.2"
+ "@esbuild/freebsd-x64" "0.24.2"
+ "@esbuild/linux-arm" "0.24.2"
+ "@esbuild/linux-arm64" "0.24.2"
+ "@esbuild/linux-ia32" "0.24.2"
+ "@esbuild/linux-loong64" "0.24.2"
+ "@esbuild/linux-mips64el" "0.24.2"
+ "@esbuild/linux-ppc64" "0.24.2"
+ "@esbuild/linux-riscv64" "0.24.2"
+ "@esbuild/linux-s390x" "0.24.2"
+ "@esbuild/linux-x64" "0.24.2"
+ "@esbuild/netbsd-arm64" "0.24.2"
+ "@esbuild/netbsd-x64" "0.24.2"
+ "@esbuild/openbsd-arm64" "0.24.2"
+ "@esbuild/openbsd-x64" "0.24.2"
+ "@esbuild/sunos-x64" "0.24.2"
+ "@esbuild/win32-arm64" "0.24.2"
+ "@esbuild/win32-ia32" "0.24.2"
+ "@esbuild/win32-x64" "0.24.2"
+
+fsevents@~2.3.2, fsevents@~2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+nanoid@^3.3.8:
+ version "3.3.8"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
+ integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
+
+picocolors@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
+ integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
+
+postcss@^8.5.1:
+ version "8.5.2"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.2.tgz#e7b99cb9d2ec3e8dd424002e7c16517cb2b846bd"
+ integrity sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==
+ dependencies:
+ nanoid "^3.3.8"
+ picocolors "^1.1.1"
+ source-map-js "^1.2.1"
+
+rollup@^4.30.1:
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.34.6.tgz#a07e4d2621759e29034d909655e7a32eee9195c9"
+ integrity sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==
+ dependencies:
+ "@types/estree" "1.0.6"
+ optionalDependencies:
+ "@rollup/rollup-android-arm-eabi" "4.34.6"
+ "@rollup/rollup-android-arm64" "4.34.6"
+ "@rollup/rollup-darwin-arm64" "4.34.6"
+ "@rollup/rollup-darwin-x64" "4.34.6"
+ "@rollup/rollup-freebsd-arm64" "4.34.6"
+ "@rollup/rollup-freebsd-x64" "4.34.6"
+ "@rollup/rollup-linux-arm-gnueabihf" "4.34.6"
+ "@rollup/rollup-linux-arm-musleabihf" "4.34.6"
+ "@rollup/rollup-linux-arm64-gnu" "4.34.6"
+ "@rollup/rollup-linux-arm64-musl" "4.34.6"
+ "@rollup/rollup-linux-loongarch64-gnu" "4.34.6"
+ "@rollup/rollup-linux-powerpc64le-gnu" "4.34.6"
+ "@rollup/rollup-linux-riscv64-gnu" "4.34.6"
+ "@rollup/rollup-linux-s390x-gnu" "4.34.6"
+ "@rollup/rollup-linux-x64-gnu" "4.34.6"
+ "@rollup/rollup-linux-x64-musl" "4.34.6"
+ "@rollup/rollup-win32-arm64-msvc" "4.34.6"
+ "@rollup/rollup-win32-ia32-msvc" "4.34.6"
+ "@rollup/rollup-win32-x64-msvc" "4.34.6"
+ fsevents "~2.3.2"
+
+source-map-js@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
+ integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
+
+vite@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-6.1.0.tgz#00a4e99a23751af98a2e4701c65ba89ce23858a6"
+ integrity sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==
+ dependencies:
+ esbuild "^0.24.2"
+ postcss "^8.5.1"
+ rollup "^4.30.1"
+ optionalDependencies:
+ fsevents "~2.3.3"
diff --git a/pkg/dashboard/frontend/test-app/nitric.yaml b/pkg/dashboard/frontend/test-app/nitric.yaml
index 0f62bf9b6..5414c25df 100644
--- a/pkg/dashboard/frontend/test-app/nitric.yaml
+++ b/pkg/dashboard/frontend/test-app/nitric.yaml
@@ -2,6 +2,22 @@ name: test-app
services:
- match: ./services/*.ts
start: yarn dev:functions $SERVICE_PATH
-
+websites:
+ - basedir: ./vite-website
+ build:
+ command: yarn install && yarn build
+ output: ./dist
+ dev:
+ command: yarn install && yarn dev --port 7850
+ url: http://localhost:7850
+ - basedir: ./docs-website
+ path: /docs
+ build:
+ command: yarn install && yarn build --base=/docs
+ output: ./dist
+ dev:
+ command: yarn install && yarn dev --port 7851 --base=/docs
+ url: http://localhost:7851/docs
preview:
- sql-databases
+ - websites
diff --git a/pkg/dashboard/frontend/test-app/vite-website/.gitignore b/pkg/dashboard/frontend/test-app/vite-website/.gitignore
new file mode 100644
index 000000000..a547bf36d
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/vite-website/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/pkg/dashboard/frontend/test-app/vite-website/index.html b/pkg/dashboard/frontend/test-app/vite-website/index.html
new file mode 100644
index 000000000..72ba3a8b3
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/vite-website/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/pkg/dashboard/frontend/test-app/vite-website/package.json b/pkg/dashboard/frontend/test-app/vite-website/package.json
new file mode 100644
index 000000000..619264888
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/vite-website/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "vite-website",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "vite": "^6.1.0"
+ }
+}
diff --git a/pkg/dashboard/frontend/test-app/vite-website/public/vite.svg b/pkg/dashboard/frontend/test-app/vite-website/public/vite.svg
new file mode 100644
index 000000000..e7b8dfb1b
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/vite-website/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pkg/dashboard/frontend/test-app/vite-website/src/counter.js b/pkg/dashboard/frontend/test-app/vite-website/src/counter.js
new file mode 100644
index 000000000..881e2d7ad
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/vite-website/src/counter.js
@@ -0,0 +1,9 @@
+export function setupCounter(element) {
+ let counter = 0
+ const setCounter = (count) => {
+ counter = count
+ element.innerHTML = `count is ${counter}`
+ }
+ element.addEventListener('click', () => setCounter(counter + 1))
+ setCounter(0)
+}
diff --git a/pkg/dashboard/frontend/test-app/vite-website/src/javascript.svg b/pkg/dashboard/frontend/test-app/vite-website/src/javascript.svg
new file mode 100644
index 000000000..f9abb2b72
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/vite-website/src/javascript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pkg/dashboard/frontend/test-app/vite-website/src/main.js b/pkg/dashboard/frontend/test-app/vite-website/src/main.js
new file mode 100644
index 000000000..17bf7e953
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/vite-website/src/main.js
@@ -0,0 +1,24 @@
+import './style.css'
+import javascriptLogo from './javascript.svg'
+import viteLogo from '/vite.svg'
+import { setupCounter } from './counter.js'
+
+document.querySelector('#app').innerHTML = `
+
+
+
+
+
+
+
+
Hello Nitric!
+
+
+
+
+ Click on the Vite logo to learn more
+
+
+`
+
+setupCounter(document.querySelector('#counter'))
diff --git a/pkg/dashboard/frontend/test-app/vite-website/src/style.css b/pkg/dashboard/frontend/test-app/vite-website/src/style.css
new file mode 100644
index 000000000..30aa81410
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/vite-website/src/style.css
@@ -0,0 +1,96 @@
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+#app {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.vanilla:hover {
+ filter: drop-shadow(0 0 2em #f7df1eaa);
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
diff --git a/pkg/dashboard/frontend/test-app/vite-website/yarn.lock b/pkg/dashboard/frontend/test-app/vite-website/yarn.lock
new file mode 100644
index 000000000..15c78083c
--- /dev/null
+++ b/pkg/dashboard/frontend/test-app/vite-website/yarn.lock
@@ -0,0 +1,327 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@esbuild/aix-ppc64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461"
+ integrity sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==
+
+"@esbuild/android-arm64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz#f592957ae8b5643129fa889c79e69cd8669bb894"
+ integrity sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==
+
+"@esbuild/android-arm@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz#72d8a2063aa630308af486a7e5cbcd1e134335b3"
+ integrity sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==
+
+"@esbuild/android-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz#9a7713504d5f04792f33be9c197a882b2d88febb"
+ integrity sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==
+
+"@esbuild/darwin-arm64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz#02ae04ad8ebffd6e2ea096181b3366816b2b5936"
+ integrity sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==
+
+"@esbuild/darwin-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz#9ec312bc29c60e1b6cecadc82bd504d8adaa19e9"
+ integrity sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==
+
+"@esbuild/freebsd-arm64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz#5e82f44cb4906d6aebf24497d6a068cfc152fa00"
+ integrity sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==
+
+"@esbuild/freebsd-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz#3fb1ce92f276168b75074b4e51aa0d8141ecce7f"
+ integrity sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==
+
+"@esbuild/linux-arm64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz#856b632d79eb80aec0864381efd29de8fd0b1f43"
+ integrity sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==
+
+"@esbuild/linux-arm@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz#c846b4694dc5a75d1444f52257ccc5659021b736"
+ integrity sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==
+
+"@esbuild/linux-ia32@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz#f8a16615a78826ccbb6566fab9a9606cfd4a37d5"
+ integrity sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==
+
+"@esbuild/linux-loong64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz#1c451538c765bf14913512c76ed8a351e18b09fc"
+ integrity sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==
+
+"@esbuild/linux-mips64el@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz#0846edeefbc3d8d50645c51869cc64401d9239cb"
+ integrity sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==
+
+"@esbuild/linux-ppc64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz#8e3fc54505671d193337a36dfd4c1a23b8a41412"
+ integrity sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==
+
+"@esbuild/linux-riscv64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz#6a1e92096d5e68f7bb10a0d64bb5b6d1daf9a694"
+ integrity sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==
+
+"@esbuild/linux-s390x@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz#ab18e56e66f7a3c49cb97d337cd0a6fea28a8577"
+ integrity sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==
+
+"@esbuild/linux-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz#8140c9b40da634d380b0b29c837a0b4267aff38f"
+ integrity sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==
+
+"@esbuild/netbsd-arm64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz#65f19161432bafb3981f5f20a7ff45abb2e708e6"
+ integrity sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==
+
+"@esbuild/netbsd-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz#7a3a97d77abfd11765a72f1c6f9b18f5396bcc40"
+ integrity sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==
+
+"@esbuild/openbsd-arm64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz#58b00238dd8f123bfff68d3acc53a6ee369af89f"
+ integrity sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==
+
+"@esbuild/openbsd-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz#0ac843fda0feb85a93e288842936c21a00a8a205"
+ integrity sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==
+
+"@esbuild/sunos-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz#8b7aa895e07828d36c422a4404cc2ecf27fb15c6"
+ integrity sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==
+
+"@esbuild/win32-arm64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz#c023afb647cabf0c3ed13f0eddfc4f1d61c66a85"
+ integrity sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==
+
+"@esbuild/win32-ia32@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz#96c356132d2dda990098c8b8b951209c3cd743c2"
+ integrity sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==
+
+"@esbuild/win32-x64@0.24.2":
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b"
+ integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==
+
+"@rollup/rollup-android-arm-eabi@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.6.tgz#9b726b4dcafb9332991e9ca49d54bafc71d9d87f"
+ integrity sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==
+
+"@rollup/rollup-android-arm64@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.6.tgz#88326ff46168a47851077ca0bf0c442689ec088f"
+ integrity sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==
+
+"@rollup/rollup-darwin-arm64@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.6.tgz#b8fbcc9389bc6fad3334a1d16dbeaaa5637c5772"
+ integrity sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==
+
+"@rollup/rollup-darwin-x64@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.6.tgz#1aa2bcad84c0fb5902e945d88822e17a4f661d51"
+ integrity sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==
+
+"@rollup/rollup-freebsd-arm64@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.6.tgz#29c54617e0929264dcb6416597d6d7481696e49f"
+ integrity sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==
+
+"@rollup/rollup-freebsd-x64@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.6.tgz#a8b58ab7d31882559d93f2d1b5863d9e4b4b2678"
+ integrity sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.6.tgz#a844e1978c8b9766b169ecb1cb5cc0d8a3f05930"
+ integrity sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==
+
+"@rollup/rollup-linux-arm-musleabihf@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.6.tgz#6b44c3b7257985d71b087fcb4ef01325e2fff201"
+ integrity sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==
+
+"@rollup/rollup-linux-arm64-gnu@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.6.tgz#ebb499cf1720115256d0c9ae7598c90cc2251bc5"
+ integrity sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==
+
+"@rollup/rollup-linux-arm64-musl@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.6.tgz#9658221b59d9e5643348f9a52fa5ef35b4dc07b1"
+ integrity sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==
+
+"@rollup/rollup-linux-loongarch64-gnu@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.6.tgz#19418cc57579a5655af2d850a89d74b3f7e9aa92"
+ integrity sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==
+
+"@rollup/rollup-linux-powerpc64le-gnu@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.6.tgz#fe0bce7778cb6ce86898c781f3f11369d1a4952c"
+ integrity sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==
+
+"@rollup/rollup-linux-riscv64-gnu@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.6.tgz#9c158360abf6e6f7794285642ba0898c580291f6"
+ integrity sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==
+
+"@rollup/rollup-linux-s390x-gnu@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.6.tgz#f9113498d22962baacdda008b5587d568b05aa34"
+ integrity sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==
+
+"@rollup/rollup-linux-x64-gnu@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.6.tgz#aec8d4cdf911cd869a72b8bd00833cb426664e0c"
+ integrity sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==
+
+"@rollup/rollup-linux-x64-musl@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.6.tgz#61c0a146bdd1b5e0dcda33690dd909b321d8f20f"
+ integrity sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==
+
+"@rollup/rollup-win32-arm64-msvc@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.6.tgz#c6c5bf290a3a459c18871110bc2e7009ce35b15a"
+ integrity sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==
+
+"@rollup/rollup-win32-ia32-msvc@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.6.tgz#16ca6bdadc9e054818b9c51f8dac82f6b8afab81"
+ integrity sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==
+
+"@rollup/rollup-win32-x64-msvc@4.34.6":
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.6.tgz#f3d03ce2d82723eb089188ea1494a719b09e1561"
+ integrity sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==
+
+"@types/estree@1.0.6":
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
+ integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
+
+esbuild@^0.24.2:
+ version "0.24.2"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.2.tgz#b5b55bee7de017bff5fb8a4e3e44f2ebe2c3567d"
+ integrity sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==
+ optionalDependencies:
+ "@esbuild/aix-ppc64" "0.24.2"
+ "@esbuild/android-arm" "0.24.2"
+ "@esbuild/android-arm64" "0.24.2"
+ "@esbuild/android-x64" "0.24.2"
+ "@esbuild/darwin-arm64" "0.24.2"
+ "@esbuild/darwin-x64" "0.24.2"
+ "@esbuild/freebsd-arm64" "0.24.2"
+ "@esbuild/freebsd-x64" "0.24.2"
+ "@esbuild/linux-arm" "0.24.2"
+ "@esbuild/linux-arm64" "0.24.2"
+ "@esbuild/linux-ia32" "0.24.2"
+ "@esbuild/linux-loong64" "0.24.2"
+ "@esbuild/linux-mips64el" "0.24.2"
+ "@esbuild/linux-ppc64" "0.24.2"
+ "@esbuild/linux-riscv64" "0.24.2"
+ "@esbuild/linux-s390x" "0.24.2"
+ "@esbuild/linux-x64" "0.24.2"
+ "@esbuild/netbsd-arm64" "0.24.2"
+ "@esbuild/netbsd-x64" "0.24.2"
+ "@esbuild/openbsd-arm64" "0.24.2"
+ "@esbuild/openbsd-x64" "0.24.2"
+ "@esbuild/sunos-x64" "0.24.2"
+ "@esbuild/win32-arm64" "0.24.2"
+ "@esbuild/win32-ia32" "0.24.2"
+ "@esbuild/win32-x64" "0.24.2"
+
+fsevents@~2.3.2, fsevents@~2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+nanoid@^3.3.8:
+ version "3.3.8"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
+ integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
+
+picocolors@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
+ integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
+
+postcss@^8.5.1:
+ version "8.5.2"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.2.tgz#e7b99cb9d2ec3e8dd424002e7c16517cb2b846bd"
+ integrity sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==
+ dependencies:
+ nanoid "^3.3.8"
+ picocolors "^1.1.1"
+ source-map-js "^1.2.1"
+
+rollup@^4.30.1:
+ version "4.34.6"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.34.6.tgz#a07e4d2621759e29034d909655e7a32eee9195c9"
+ integrity sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==
+ dependencies:
+ "@types/estree" "1.0.6"
+ optionalDependencies:
+ "@rollup/rollup-android-arm-eabi" "4.34.6"
+ "@rollup/rollup-android-arm64" "4.34.6"
+ "@rollup/rollup-darwin-arm64" "4.34.6"
+ "@rollup/rollup-darwin-x64" "4.34.6"
+ "@rollup/rollup-freebsd-arm64" "4.34.6"
+ "@rollup/rollup-freebsd-x64" "4.34.6"
+ "@rollup/rollup-linux-arm-gnueabihf" "4.34.6"
+ "@rollup/rollup-linux-arm-musleabihf" "4.34.6"
+ "@rollup/rollup-linux-arm64-gnu" "4.34.6"
+ "@rollup/rollup-linux-arm64-musl" "4.34.6"
+ "@rollup/rollup-linux-loongarch64-gnu" "4.34.6"
+ "@rollup/rollup-linux-powerpc64le-gnu" "4.34.6"
+ "@rollup/rollup-linux-riscv64-gnu" "4.34.6"
+ "@rollup/rollup-linux-s390x-gnu" "4.34.6"
+ "@rollup/rollup-linux-x64-gnu" "4.34.6"
+ "@rollup/rollup-linux-x64-musl" "4.34.6"
+ "@rollup/rollup-win32-arm64-msvc" "4.34.6"
+ "@rollup/rollup-win32-ia32-msvc" "4.34.6"
+ "@rollup/rollup-win32-x64-msvc" "4.34.6"
+ fsevents "~2.3.2"
+
+source-map-js@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
+ integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
+
+vite@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-6.1.0.tgz#00a4e99a23751af98a2e4701c65ba89ce23858a6"
+ integrity sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==
+ dependencies:
+ esbuild "^0.24.2"
+ postcss "^8.5.1"
+ rollup "^4.30.1"
+ optionalDependencies:
+ fsevents "~2.3.3"
From ac374b3e20f0bbb8c589e459f7a4dc8d64865542 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Fri, 14 Feb 2025 17:24:35 +1100
Subject: [PATCH 13/31] install websites before running tests
---
.github/workflows/dashboard-run-test.yaml | 1 +
.github/workflows/dashboard-start-test.yaml | 1 +
pkg/dashboard/frontend/cypress/e2e/logs.cy.ts | 2 +-
pkg/dashboard/frontend/cypress/e2e/websites.cy.ts | 2 +-
pkg/dashboard/frontend/test-app/nitric.yaml | 4 ++--
pkg/dashboard/frontend/test-app/package.json | 3 ++-
6 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/dashboard-run-test.yaml b/.github/workflows/dashboard-run-test.yaml
index 1a91819e6..deedce4ce 100644
--- a/.github/workflows/dashboard-run-test.yaml
+++ b/.github/workflows/dashboard-run-test.yaml
@@ -44,6 +44,7 @@ jobs:
run: |
cd ${{ github.workspace }}/cli/pkg/dashboard/frontend/test-app
yarn install
+ yarn install:websites
nitric run --ci &
sleep 15
diff --git a/.github/workflows/dashboard-start-test.yaml b/.github/workflows/dashboard-start-test.yaml
index 9eca8e3ab..d56c84575 100644
--- a/.github/workflows/dashboard-start-test.yaml
+++ b/.github/workflows/dashboard-start-test.yaml
@@ -51,6 +51,7 @@ jobs:
run: |
cd ${{ github.workspace }}/cli/pkg/dashboard/frontend/test-app
yarn install
+ yarn install:websites
nitric start --ci &
- name: Run Tests
diff --git a/pkg/dashboard/frontend/cypress/e2e/logs.cy.ts b/pkg/dashboard/frontend/cypress/e2e/logs.cy.ts
index 02ce66624..5e85e8cd5 100644
--- a/pkg/dashboard/frontend/cypress/e2e/logs.cy.ts
+++ b/pkg/dashboard/frontend/cypress/e2e/logs.cy.ts
@@ -46,7 +46,7 @@ describe('logs test suite', () => {
cy.get('div[data-value="nitric"]').click()
- cy.getTestEl('logs').children().should('have.length', 3)
+ cy.getTestEl('logs').children().should('have.length.above', 3)
cy.getTestEl('filter-logs-reset-btn').click()
diff --git a/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts b/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
index e903d9427..730dee485 100644
--- a/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
+++ b/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
@@ -22,7 +22,7 @@ describe('Websites Spec', () => {
const pathMap = {
'vite-website': '',
- 'docs-website': 'docs',
+ 'docs-website': 'docs/',
}
const url = `http://localhost:5000/${pathMap[id]}`
diff --git a/pkg/dashboard/frontend/test-app/nitric.yaml b/pkg/dashboard/frontend/test-app/nitric.yaml
index 5414c25df..0be0f9d6e 100644
--- a/pkg/dashboard/frontend/test-app/nitric.yaml
+++ b/pkg/dashboard/frontend/test-app/nitric.yaml
@@ -13,10 +13,10 @@ websites:
- basedir: ./docs-website
path: /docs
build:
- command: yarn install && yarn build --base=/docs
+ command: yarn build --base=/docs
output: ./dist
dev:
- command: yarn install && yarn dev --port 7851 --base=/docs
+ command: yarn dev --port 7851 --base=/docs
url: http://localhost:7851/docs
preview:
- sql-databases
diff --git a/pkg/dashboard/frontend/test-app/package.json b/pkg/dashboard/frontend/test-app/package.json
index 17ac5d705..dc9b25b04 100644
--- a/pkg/dashboard/frontend/test-app/package.json
+++ b/pkg/dashboard/frontend/test-app/package.json
@@ -20,6 +20,7 @@
"typescript": "^4.8.3"
},
"scripts": {
- "dev:functions": "nodemon -r dotenv/config"
+ "dev:functions": "nodemon -r dotenv/config",
+ "install:websites": "yarn --cwd ./vite-website install && yarn --cwd ./docs-website install"
}
}
From 7e25dce70c2ca269eff1a2b1cb286576a76d1330 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Fri, 14 Feb 2025 17:29:01 +1100
Subject: [PATCH 14/31] update node version for websites
---
.github/workflows/dashboard-run-test.yaml | 2 +-
.github/workflows/dashboard-start-test.yaml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/dashboard-run-test.yaml b/.github/workflows/dashboard-run-test.yaml
index deedce4ce..f88455265 100644
--- a/.github/workflows/dashboard-run-test.yaml
+++ b/.github/workflows/dashboard-run-test.yaml
@@ -27,7 +27,7 @@ jobs:
- uses: actions/setup-node@v4
with:
- node-version: 21
+ node-version: 22
- name: Setup Go
uses: actions/setup-go@v3
diff --git a/.github/workflows/dashboard-start-test.yaml b/.github/workflows/dashboard-start-test.yaml
index d56c84575..7becd1ad9 100644
--- a/.github/workflows/dashboard-start-test.yaml
+++ b/.github/workflows/dashboard-start-test.yaml
@@ -27,7 +27,7 @@ jobs:
- uses: actions/setup-node@v4
with:
- node-version: 21
+ node-version: 22
- name: Lint Dashboard
working-directory: cli/pkg/dashboard/frontend
From 62ab49e724744c2d357af416c7a881ba31ce854a Mon Sep 17 00:00:00 2001
From: David Moore
Date: Fri, 14 Feb 2025 17:38:07 +1100
Subject: [PATCH 15/31] more test fixes
---
pkg/dashboard/frontend/cypress/e2e/logs.cy.ts | 2 +-
pkg/dashboard/frontend/cypress/e2e/websites.cy.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/pkg/dashboard/frontend/cypress/e2e/logs.cy.ts b/pkg/dashboard/frontend/cypress/e2e/logs.cy.ts
index 5e85e8cd5..3592fc092 100644
--- a/pkg/dashboard/frontend/cypress/e2e/logs.cy.ts
+++ b/pkg/dashboard/frontend/cypress/e2e/logs.cy.ts
@@ -46,7 +46,7 @@ describe('logs test suite', () => {
cy.get('div[data-value="nitric"]').click()
- cy.getTestEl('logs').children().should('have.length.above', 3)
+ cy.getTestEl('logs').children().should('have.length.at.least', 3)
cy.getTestEl('filter-logs-reset-btn').click()
diff --git a/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts b/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
index 730dee485..e903d9427 100644
--- a/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
+++ b/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
@@ -22,7 +22,7 @@ describe('Websites Spec', () => {
const pathMap = {
'vite-website': '',
- 'docs-website': 'docs/',
+ 'docs-website': 'docs',
}
const url = `http://localhost:5000/${pathMap[id]}`
From 9fa1510811b999ed2a2927ad8f219fcb6f5ab8d6 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Fri, 14 Feb 2025 17:51:30 +1100
Subject: [PATCH 16/31] adjust test settings
---
.github/workflows/dashboard-run-test.yaml | 2 +-
.github/workflows/dashboard-start-test.yaml | 1 +
pkg/dashboard/frontend/cypress.config.ts | 1 +
pkg/dashboard/frontend/cypress/e2e/websites.cy.ts | 2 +-
4 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/dashboard-run-test.yaml b/.github/workflows/dashboard-run-test.yaml
index f88455265..5eb7b918b 100644
--- a/.github/workflows/dashboard-run-test.yaml
+++ b/.github/workflows/dashboard-run-test.yaml
@@ -46,7 +46,7 @@ jobs:
yarn install
yarn install:websites
nitric run --ci &
- sleep 15
+ sleep 25
- name: Run Tests
uses: cypress-io/github-action@v5
diff --git a/.github/workflows/dashboard-start-test.yaml b/.github/workflows/dashboard-start-test.yaml
index 7becd1ad9..1fc03d4c0 100644
--- a/.github/workflows/dashboard-start-test.yaml
+++ b/.github/workflows/dashboard-start-test.yaml
@@ -53,6 +53,7 @@ jobs:
yarn install
yarn install:websites
nitric start --ci &
+ sleep 10
- name: Run Tests
uses: cypress-io/github-action@v5
diff --git a/pkg/dashboard/frontend/cypress.config.ts b/pkg/dashboard/frontend/cypress.config.ts
index 69754451b..28a8837e4 100644
--- a/pkg/dashboard/frontend/cypress.config.ts
+++ b/pkg/dashboard/frontend/cypress.config.ts
@@ -6,5 +6,6 @@ export default defineConfig({
setupNodeEvents(on, config) {
// implement node event listeners here
},
+ chromeWebSecurity: false,
},
})
diff --git a/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts b/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
index e903d9427..bc5ae70db 100644
--- a/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
+++ b/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
@@ -39,7 +39,7 @@ describe('Websites Spec', () => {
const title = titleMap[id]
- cy.origin(url, { args: { title } }, ({ title }) => {
+ cy.origin('localhost:5000', { args: { title } }, ({ title }) => {
cy.get('h1').should('have.text', title)
})
})
From 9ed790ddcd9824394dae73e85097409e3fa35251 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Fri, 14 Feb 2025 17:56:22 +1100
Subject: [PATCH 17/31] remove install commands from base site
---
pkg/dashboard/frontend/test-app/nitric.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pkg/dashboard/frontend/test-app/nitric.yaml b/pkg/dashboard/frontend/test-app/nitric.yaml
index 0be0f9d6e..a747ec3c3 100644
--- a/pkg/dashboard/frontend/test-app/nitric.yaml
+++ b/pkg/dashboard/frontend/test-app/nitric.yaml
@@ -5,10 +5,10 @@ services:
websites:
- basedir: ./vite-website
build:
- command: yarn install && yarn build
+ command: yarn build
output: ./dist
dev:
- command: yarn install && yarn dev --port 7850
+ command: yarn dev --port 7850
url: http://localhost:7850
- basedir: ./docs-website
path: /docs
From 01463871a1f023a4a03ce6926dfefc3cce12be8c Mon Sep 17 00:00:00 2001
From: David Moore
Date: Fri, 14 Feb 2025 18:03:03 +1100
Subject: [PATCH 18/31] make origin http for test
---
pkg/dashboard/frontend/cypress/e2e/websites.cy.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts b/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
index bc5ae70db..edc3b76ba 100644
--- a/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
+++ b/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
@@ -39,7 +39,7 @@ describe('Websites Spec', () => {
const title = titleMap[id]
- cy.origin('localhost:5000', { args: { title } }, ({ title }) => {
+ cy.origin('http://localhost:5000', { args: { title } }, ({ title }) => {
cy.get('h1').should('have.text', title)
})
})
From 8b9874fd816779fba1525fc8ccc26a3045bbd7c8 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Tue, 25 Feb 2025 16:03:57 +1100
Subject: [PATCH 19/31] sort websites array for dashboard
---
pkg/dashboard/dashboard.go | 6 ++++++
pkg/dashboard/frontend/cypress/e2e/websites.cy.ts | 2 +-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/pkg/dashboard/dashboard.go b/pkg/dashboard/dashboard.go
index 570197175..eb0ba9372 100644
--- a/pkg/dashboard/dashboard.go
+++ b/pkg/dashboard/dashboard.go
@@ -657,6 +657,12 @@ func (d *Dashboard) handleWebsites(state websites.State) {
})
}
+ if len(websites) > 0 {
+ slices.SortFunc(websites, func(a, b WebsiteSpec) int {
+ return compare(a.Name, b.Name)
+ })
+ }
+
d.websites = websites
d.refresh()
diff --git a/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts b/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
index edc3b76ba..9bf2e958f 100644
--- a/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
+++ b/pkg/dashboard/frontend/cypress/e2e/websites.cy.ts
@@ -7,7 +7,7 @@ describe('Websites Spec', () => {
it('should retrieve correct websites in list', () => {
cy.visit('/websites')
- cy.get('h2').should('contain.text', 'vite-website')
+ cy.get('h2').should('contain.text', 'docs-website')
expectedWebsites.forEach((id) => {
cy.get(`[data-rct-item-id="${id}"]`).should('exist')
From 169a37d3ac8fa805975db001a78e63bc2a601edf Mon Sep 17 00:00:00 2001
From: David Moore
Date: Wed, 26 Feb 2025 16:34:25 +1100
Subject: [PATCH 20/31] improve error messages for website dev server
---
pkg/cloud/websites/websites.go | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/pkg/cloud/websites/websites.go b/pkg/cloud/websites/websites.go
index 0290f58d7..c29e1e6bd 100644
--- a/pkg/cloud/websites/websites.go
+++ b/pkg/cloud/websites/websites.go
@@ -17,6 +17,7 @@
package websites
import (
+ "errors"
"fmt"
"maps"
"net"
@@ -109,7 +110,7 @@ func (h staticSiteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Target backend API server
target, err := url.Parse(h.devURL)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ http.Error(w, fmt.Sprintf("Invalid dev URL '%s': %v", h.devURL, err), http.StatusInternalServerError)
return
}
@@ -120,9 +121,16 @@ func (h staticSiteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Reverse proxy request
proxy := httputil.NewSingleHostReverseProxy(target)
+
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
if err != nil {
- http.Error(w, err.Error(), http.StatusBadGateway)
+ var opErr *net.OpError
+
+ if errors.As(err, &opErr) && opErr.Op == "dial" {
+ http.Error(w, "Connection to the dev server was refused. Check the URL and server status.", http.StatusServiceUnavailable)
+ } else {
+ http.Error(w, err.Error(), http.StatusBadGateway)
+ }
}
}
proxy.ServeHTTP(w, r)
From 829e2988289cb6e4ffc564523839a325a1f93425 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Wed, 26 Feb 2025 17:41:42 +1100
Subject: [PATCH 21/31] update to use latest proto
---
go.mod | 2 +-
go.sum | 8 ++------
pkg/cloud/websites/websites.go | 9 +++++----
pkg/collector/spec.go | 8 +++++++-
pkg/project/project.go | 29 +++++++++++++++++------------
pkg/project/website.go | 2 +-
6 files changed, 33 insertions(+), 25 deletions(-)
diff --git a/go.mod b/go.mod
index 3e02fb68c..71d2aaf5e 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
github.com/hashicorp/consul/sdk v0.13.0
github.com/hashicorp/go-getter v1.6.2
github.com/hashicorp/go-version v1.7.0
- github.com/nitrictech/nitric/core v0.0.0-20250123074029-0306df1e20ae
+ github.com/nitrictech/nitric/core v0.0.0-20250226055017-c8d108e2f62f
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.8.1
github.com/valyala/fasthttp v1.55.0
diff --git a/go.sum b/go.sum
index f3aaaafb1..3f4d05cc5 100644
--- a/go.sum
+++ b/go.sum
@@ -585,12 +585,8 @@ github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm
github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
github.com/nitrictech/nitric/cloud/common v0.0.0-20241003062412-76ea6275fb0b h1:wZeUrnmhYjdhSuL6ov+kVfuFJC9H14sk0kzEpt6aRoo=
github.com/nitrictech/nitric/cloud/common v0.0.0-20241003062412-76ea6275fb0b/go.mod h1:ZsCdb3xbukhXAp9ZNbV6qWJqRC+eLkxhXy8bhs/cC2A=
-github.com/nitrictech/nitric/core v0.0.0-20250123065014-599bda2a2582 h1:dKVFR/rquvNB/FNu8GFAZJ5RcW7HrgyE+I8rdmvKwYc=
-github.com/nitrictech/nitric/core v0.0.0-20250123065014-599bda2a2582/go.mod h1:3kPpyO2oZEGfurDVsTh9XTg43b/4JAIbLkaktEvRF58=
-github.com/nitrictech/nitric/core v0.0.0-20250123070044-4b31d7498e96 h1:GdRQEkMYrZehM2SKkUecR2/SPa5TuKpxuuZetShZnAI=
-github.com/nitrictech/nitric/core v0.0.0-20250123070044-4b31d7498e96/go.mod h1:3kPpyO2oZEGfurDVsTh9XTg43b/4JAIbLkaktEvRF58=
-github.com/nitrictech/nitric/core v0.0.0-20250123074029-0306df1e20ae h1:hpSGt8KQ4OPPLqGvf5sFTv7h9isngWQLveVcfW4Z5i4=
-github.com/nitrictech/nitric/core v0.0.0-20250123074029-0306df1e20ae/go.mod h1:3kPpyO2oZEGfurDVsTh9XTg43b/4JAIbLkaktEvRF58=
+github.com/nitrictech/nitric/core v0.0.0-20250226055017-c8d108e2f62f h1:Aujlui7gF5ZiUhg0Z1vZ8tymsf37g/VwDJJI2gL8hpE=
+github.com/nitrictech/nitric/core v0.0.0-20250226055017-c8d108e2f62f/go.mod h1:3kPpyO2oZEGfurDVsTh9XTg43b/4JAIbLkaktEvRF58=
github.com/nunnatsa/ginkgolinter v0.16.2 h1:8iLqHIZvN4fTLDC0Ke9tbSZVcyVHoBs0HIbnVSxfHJk=
github.com/nunnatsa/ginkgolinter v0.16.2/go.mod h1:4tWRinDN1FeJgU+iJANW/kz7xKN5nYRAOfJDQUS9dOQ=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
diff --git a/pkg/cloud/websites/websites.go b/pkg/cloud/websites/websites.go
index c29e1e6bd..0de78fde8 100644
--- a/pkg/cloud/websites/websites.go
+++ b/pkg/cloud/websites/websites.go
@@ -40,10 +40,11 @@ type WebsitePb = deploymentspb.Website
type Website struct {
*WebsitePb
- Name string
- Directory string
- DevURL string
- URL string
+ Name string
+ Directory string
+ OutputDirectory string
+ DevURL string
+ URL string
}
type (
diff --git a/pkg/collector/spec.go b/pkg/collector/spec.go
index 2441b9534..6ddde5bcb 100644
--- a/pkg/collector/spec.go
+++ b/pkg/collector/spec.go
@@ -1183,7 +1183,13 @@ func ServiceRequirementsToSpec(projectName string, environmentVariables map[stri
}
for _, website := range websiteRequirements {
- cleanedPath := strings.TrimRight(website.OutputDirectory, string(os.PathSeparator))
+ localDir, ok := website.AssetSource.(*deploymentspb.Website_LocalDirectory)
+ if !ok {
+ projectErrors.Add(fmt.Errorf("website asset source must be a local directory"))
+ continue
+ }
+
+ cleanedPath := strings.TrimRight(localDir.LocalDirectory, string(os.PathSeparator))
// Get the parent directory
parentDir := filepath.Dir(cleanedPath)
// Extract the directory name from the parent path
diff --git a/pkg/project/project.go b/pkg/project/project.go
index 81025bfba..cc60e2537 100644
--- a/pkg/project/project.go
+++ b/pkg/project/project.go
@@ -461,10 +461,12 @@ func (p *Project) CollectWebsiteRequirements() ([]*deploymentpb.Website, error)
}
allWebsiteRequirements = append(allWebsiteRequirements, &deploymentpb.Website{
- BasePath: site.path,
- OutputDirectory: outputDir,
- IndexDocument: site.indexPage,
- ErrorDocument: site.errorPage,
+ BasePath: site.path,
+ IndexDocument: site.indexPage,
+ ErrorDocument: site.errorPage,
+ AssetSource: &deploymentpb.Website_LocalDirectory{
+ LocalDirectory: outputDir,
+ },
})
}
@@ -619,20 +621,23 @@ func (p *Project) RunWebsites(localCloud *cloud.LocalCloud) error {
return fmt.Errorf("unable to get absolute output path for website %s: %w", site.basedir, err)
}
- directory, err := site.GetAbsoluteDirectory()
+ absBaseDir, err := site.GetAbsoluteBaseDirectory()
if err != nil {
return fmt.Errorf("unable to get absolute directory for website %s: %w", site.basedir, err)
}
sites = append(sites, websites.Website{
- Name: site.Name,
- DevURL: site.devURL,
- Directory: directory,
+ Name: site.Name,
+ DevURL: site.devURL,
+ Directory: absBaseDir,
+ OutputDirectory: outputDir,
WebsitePb: &websites.WebsitePb{
- BasePath: site.path,
- OutputDirectory: outputDir,
- IndexDocument: site.indexPage,
- ErrorDocument: site.errorPage,
+ BasePath: site.path,
+ IndexDocument: site.indexPage,
+ ErrorDocument: site.errorPage,
+ AssetSource: &deploymentpb.Website_LocalDirectory{
+ LocalDirectory: outputDir,
+ },
},
})
}
diff --git a/pkg/project/website.go b/pkg/project/website.go
index 220663e0c..d53426124 100644
--- a/pkg/project/website.go
+++ b/pkg/project/website.go
@@ -61,7 +61,7 @@ func (s *Website) GetAbsoluteOutputPath() (string, error) {
return filepath.Abs(s.GetOutputPath())
}
-func (s *Website) GetAbsoluteDirectory() (string, error) {
+func (s *Website) GetAbsoluteBaseDirectory() (string, error) {
return filepath.Abs(s.basedir)
}
From 1c34081b4336168120a51a25c3f8d7634d862d50 Mon Sep 17 00:00:00 2001
From: Jye Cusch
Date: Thu, 27 Feb 2025 13:32:14 +1100
Subject: [PATCH 22/31] update mode naming
---
cmd/run.go | 2 +-
cmd/start.go | 2 +-
pkg/cloud/cloud.go | 59 +++++++++++++++++++-------------------
pkg/dashboard/dashboard.go | 6 ++--
4 files changed, 34 insertions(+), 35 deletions(-)
diff --git a/cmd/run.go b/cmd/run.go
index 6cbe38618..4c147f2eb 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -106,7 +106,7 @@ var runCmd = &cobra.Command{
LogWriter: logWriter,
LocalConfig: proj.LocalConfig,
MigrationRunner: project.BuildAndRunMigrations,
- LocalCloudMode: cloud.LocalCloudModeRun,
+ LocalCloudMode: cloud.RunMode,
})
tui.CheckErr(err)
runView.Send(local.LocalCloudStartStatusMsg{Status: local.Done})
diff --git a/cmd/start.go b/cmd/start.go
index e5c7e9742..eff736c93 100644
--- a/cmd/start.go
+++ b/cmd/start.go
@@ -186,7 +186,7 @@ var startCmd = &cobra.Command{
LogWriter: logWriter,
LocalConfig: proj.LocalConfig,
MigrationRunner: project.BuildAndRunMigrations,
- LocalCloudMode: cloud.LocalCloudModeStart,
+ LocalCloudMode: cloud.StartMode,
})
tui.CheckErr(err)
runView.Send(local.LocalCloudStartStatusMsg{Status: local.Done})
diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go
index 3d13a1a5f..27dff6bc4 100644
--- a/pkg/cloud/cloud.go
+++ b/pkg/cloud/cloud.go
@@ -53,20 +53,19 @@ type Subscribable[T any, A any] interface {
type ServiceName = string
-// LocalCloudMode type run or start
-type LocalCloudMode string
+type Mode string
const (
- // LocalCloudModeRun - run mode
- LocalCloudModeRun LocalCloudMode = "run"
- // LocalCloudModeStart - start mode
- LocalCloudModeStart LocalCloudMode = "start"
+ // RunMode - services run directly on the host machine
+ RunMode Mode = "run"
+ // StartMode - services run in containers
+ StartMode Mode = "start"
)
type LocalCloud struct {
- serverLock sync.Mutex
- servers map[ServiceName]*server.NitricServer
- localCloudMode LocalCloudMode
+ serverLock sync.Mutex
+ servers map[ServiceName]*server.NitricServer
+ mode Mode
Apis *apis.LocalApiGatewayService
Batch *batch.LocalBatchService
@@ -84,8 +83,8 @@ type LocalCloud struct {
Databases *sql.LocalSqlServer
}
-func (lc *LocalCloud) GetLocalCloudMode() LocalCloudMode {
- return lc.localCloudMode
+func (lc *LocalCloud) GetMode() Mode {
+ return lc.mode
}
// StartLocalNitric - starts the Nitric Server, including plugins and their local dependencies (e.g. local versions of cloud services)
@@ -252,7 +251,7 @@ type LocalCloudOptions struct {
LogWriter io.Writer
LocalConfig localconfig.LocalConfiguration
MigrationRunner sql.MigrationRunner
- LocalCloudMode LocalCloudMode
+ LocalCloudMode Mode
}
func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) {
@@ -313,7 +312,7 @@ func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) {
connectionStringHost := "localhost"
// Use the host.docker.internal address for connection strings with local cloud run mode
- if opts.LocalCloudMode == LocalCloudModeRun {
+ if opts.LocalCloudMode == RunMode {
connectionStringHost = dockerhost.GetInternalDockerHost()
}
@@ -322,24 +321,24 @@ func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) {
return nil, err
}
- localWebsites := websites.NewLocalWebsitesService(localGateway.GetApiAddress, opts.LocalCloudMode == LocalCloudModeStart)
+ localWebsites := websites.NewLocalWebsitesService(localGateway.GetApiAddress, opts.LocalCloudMode == StartMode)
return &LocalCloud{
- servers: make(map[string]*server.NitricServer),
- localCloudMode: opts.LocalCloudMode,
- Apis: localApis,
- Batch: localBatch,
- Http: localHttpProxy,
- Resources: localResources,
- Schedules: localSchedules,
- Storage: localStorage,
- Topics: localTopics,
- Websockets: localWebsockets,
- Websites: localWebsites,
- Gateway: localGateway,
- Secrets: localSecrets,
- KeyValue: keyvalueService,
- Queues: localQueueService,
- Databases: localDatabaseService,
+ servers: make(map[string]*server.NitricServer),
+ mode: opts.LocalCloudMode,
+ Apis: localApis,
+ Batch: localBatch,
+ Http: localHttpProxy,
+ Resources: localResources,
+ Schedules: localSchedules,
+ Storage: localStorage,
+ Topics: localTopics,
+ Websockets: localWebsockets,
+ Websites: localWebsites,
+ Gateway: localGateway,
+ Secrets: localSecrets,
+ KeyValue: keyvalueService,
+ Queues: localQueueService,
+ Databases: localDatabaseService,
}, nil
}
diff --git a/pkg/dashboard/dashboard.go b/pkg/dashboard/dashboard.go
index eb0ba9372..d9759caa4 100644
--- a/pkg/dashboard/dashboard.go
+++ b/pkg/dashboard/dashboard.go
@@ -166,7 +166,7 @@ type WebsiteSpec struct {
type Dashboard struct {
resourcesLock sync.Mutex
- localCloudMode cloud.LocalCloudMode
+ localCloudMode cloud.Mode
project *project.Project
storageService *storage.LocalStorageService
gatewayService *gateway.LocalGatewayService
@@ -232,7 +232,7 @@ type DashboardResponse struct {
CurrentVersion string `json:"currentVersion"`
LatestVersion string `json:"latestVersion"`
Connected bool `json:"connected"`
- LocalCloudMode cloud.LocalCloudMode `json:"localCloudMode"`
+ LocalCloudMode cloud.Mode `json:"localCloudMode"`
}
type Bucket struct {
@@ -934,7 +934,7 @@ func New(noBrowser bool, localCloud *cloud.LocalCloud, project *project.Project)
wsWebSocket := melody.New()
dash := &Dashboard{
- localCloudMode: localCloud.GetLocalCloudMode(),
+ localCloudMode: localCloud.GetMode(),
project: project,
storageService: localCloud.Storage,
gatewayService: localCloud.Gateway,
From 22979b0d64fa60cdd9ede240b440cf04ae739dcc Mon Sep 17 00:00:00 2001
From: Jye Cusch
Date: Thu, 27 Feb 2025 14:07:20 +1100
Subject: [PATCH 23/31] minor refactor
---
pkg/cloud/websites/websites.go | 118 ++++++++++++++++-----------------
1 file changed, 57 insertions(+), 61 deletions(-)
diff --git a/pkg/cloud/websites/websites.go b/pkg/cloud/websites/websites.go
index 0de78fde8..69ebbebef 100644
--- a/pkg/cloud/websites/websites.go
+++ b/pkg/cloud/websites/websites.go
@@ -79,13 +79,15 @@ func (l *LocalWebsiteService) register(website Website) {
l.websiteRegLock.Lock()
defer l.websiteRegLock.Unlock()
- // add URL to the website and store it in the state
+ // Emulates the CDN URL used in a deployed environment
+ publicUrl := fmt.Sprintf("http://localhost:%d/%s", l.port, strings.TrimPrefix(website.BasePath, "/"))
+
l.state[website.Name] = Website{
WebsitePb: website.WebsitePb,
Name: website.Name,
DevURL: website.DevURL,
Directory: website.Directory,
- URL: fmt.Sprintf("http://localhost:%d/%s", l.port, strings.TrimPrefix(website.BasePath, "/")),
+ URL: publicUrl,
}
l.publishState()
@@ -98,76 +100,78 @@ type staticSiteHandler struct {
isStartCmd bool
}
-// ServeHTTP - Serve a static website from the local filesystem
-func (h staticSiteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- // if start command just proxy the request to the dev url
- if h.isStartCmd {
- // if the dev url is not set, return a 500 internal server error with a message
- if h.devURL == "" {
- http.Error(w, "The dev URL is not set for this website", http.StatusInternalServerError)
- return
- }
+func (h staticSiteHandler) serveProxy(res http.ResponseWriter, req *http.Request) {
+ if h.devURL == "" {
+ http.Error(res, "The dev URL is not set for this website", http.StatusInternalServerError)
+ return
+ }
- // Target backend API server
- target, err := url.Parse(h.devURL)
- if err != nil {
- http.Error(w, fmt.Sprintf("Invalid dev URL '%s': %v", h.devURL, err), http.StatusInternalServerError)
- return
- }
+ targetUrl, err := url.Parse(h.devURL)
+ if err != nil {
+ http.Error(res, fmt.Sprintf("Invalid dev URL '%s': %v", h.devURL, err), http.StatusInternalServerError)
+ return
+ }
- // ignore proxy errors like unsupported protocol
- if target == nil || target.Scheme == "" {
- return
- }
+ // ignore proxy errors like unsupported protocol
+ if targetUrl == nil || targetUrl.Scheme == "" {
+ return
+ }
- // Reverse proxy request
- proxy := httputil.NewSingleHostReverseProxy(target)
+ // Reverse proxy request
+ proxy := httputil.NewSingleHostReverseProxy(targetUrl)
- proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
- if err != nil {
- var opErr *net.OpError
+ proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
+ if err != nil {
+ var opErr *net.OpError
- if errors.As(err, &opErr) && opErr.Op == "dial" {
- http.Error(w, "Connection to the dev server was refused. Check the URL and server status.", http.StatusServiceUnavailable)
- } else {
- http.Error(w, err.Error(), http.StatusBadGateway)
- }
+ if errors.As(err, &opErr) && opErr.Op == "dial" {
+ http.Error(w, "Connection to the dev server was refused. Check the URL and server status.", http.StatusServiceUnavailable)
+ } else {
+ http.Error(w, err.Error(), http.StatusBadGateway)
}
}
- proxy.ServeHTTP(w, r)
-
- return
}
+ proxy.ServeHTTP(res, req)
+}
- path := filepath.Join(h.website.OutputDirectory, r.URL.Path)
+func (h staticSiteHandler) serveStatic(res http.ResponseWriter, req *http.Request) {
+ path := filepath.Join(h.website.OutputDirectory, req.URL.Path)
- // check whether a file exists or is a directory at the given path
fi, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
// if the file doesn't exist, serve the error page with a 404 status code
- http.ServeFile(w, r, filepath.Join(h.website.OutputDirectory, h.website.ErrorDocument))
+ http.ServeFile(res, req, filepath.Join(h.website.OutputDirectory, h.website.ErrorDocument))
+
return
}
- // if we got an error (that wasn't that the file doesn't exist) stating the
- // file, return a 500 internal server error and stop
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ http.Error(res, err.Error(), http.StatusInternalServerError)
return
}
if fi.IsDir() {
- http.ServeFile(w, r, filepath.Join(h.website.OutputDirectory, h.website.IndexDocument))
+ http.ServeFile(res, req, filepath.Join(h.website.OutputDirectory, h.website.IndexDocument))
return
}
- // otherwise, use http.FileServer to serve the static file
- http.FileServer(http.Dir(h.website.OutputDirectory)).ServeHTTP(w, r)
+ http.FileServer(http.Dir(h.website.OutputDirectory)).ServeHTTP(res, req)
}
-// Serve - Serve a website from the local filesystem
+// ServeHTTP - Serve a static website from the local filesystem
+func (h staticSiteHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
+ // If the website is running (i.e. start mode), proxy the request to the dev server
+ if h.isStartCmd {
+ h.serveProxy(res, req)
+ return
+ }
+
+ h.serveStatic(res, req)
+}
+
+// Start - Start the local website service
func (l *LocalWebsiteService) Start(websites []Website) error {
newLis, err := netx.GetNextListener(netx.MinPort(5000))
if err != nil {
@@ -178,33 +182,25 @@ func (l *LocalWebsiteService) Start(websites []Website) error {
_ = newLis.Close()
- // Initialize the multiplexer only if websites will be served
mux := http.NewServeMux()
- // Register the API handler
- mux.HandleFunc("/api/{name}/", func(w http.ResponseWriter, r *http.Request) {
- // get the api name from the request path
- apiName := r.PathValue("name")
+ // Register the API proxy handler
+ mux.HandleFunc("/api/{name}/", func(res http.ResponseWriter, req *http.Request) {
+ apiName := req.PathValue("name")
- // get the address of the api
apiAddress := l.getApiAddress(apiName)
if apiAddress == "" {
- http.Error(w, fmt.Sprintf("api %s not found", apiName), http.StatusNotFound)
+ http.Error(res, fmt.Sprintf("api %s not found", apiName), http.StatusNotFound)
return
}
- // Strip /api/{name}/ from the URL path
- newPath := strings.TrimPrefix(r.URL.Path, fmt.Sprintf("/api/%s", apiName))
-
- // Target backend API server
- target, _ := url.Parse(apiAddress)
+ targetPath := strings.TrimPrefix(req.URL.Path, fmt.Sprintf("/api/%s", apiName))
+ targetUrl, _ := url.Parse(apiAddress)
- // Reverse proxy request
- proxy := httputil.NewSingleHostReverseProxy(target)
- r.URL.Path = newPath
+ proxy := httputil.NewSingleHostReverseProxy(targetUrl)
+ req.URL.Path = targetPath
- // Forward the modified request to the backend
- proxy.ServeHTTP(w, r)
+ proxy.ServeHTTP(res, req)
})
// Register the SPA handler for each website
From 6acdddcd1551321acca57cdb6939baacba539454 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Wed, 5 Mar 2025 12:18:56 +1100
Subject: [PATCH 24/31] add check for root website
---
pkg/project/project.go | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/pkg/project/project.go b/pkg/project/project.go
index cc60e2537..ff655c922 100644
--- a/pkg/project/project.go
+++ b/pkg/project/project.go
@@ -854,6 +854,14 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *
return website.path
})
+ // check that there is a root website
+ _, found := lo.Find(websites, func(website Website) bool {
+ return website.path == "/"
+ })
+ if !found {
+ return nil, fmt.Errorf("no root website found, please add a website with path /")
+ }
+
if len(siteDuplicates) > 0 {
duplicatePaths := lo.Map(siteDuplicates, func(website Website, i int) string {
return website.path
From e3a3ddcc3e91b83df6a41c9e99055d1077372f1c Mon Sep 17 00:00:00 2001
From: David Moore
Date: Wed, 5 Mar 2025 12:35:49 +1100
Subject: [PATCH 25/31] ensure /api is not used as a route
---
pkg/project/project.go | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/pkg/project/project.go b/pkg/project/project.go
index ff655c922..e4cac7836 100644
--- a/pkg/project/project.go
+++ b/pkg/project/project.go
@@ -862,6 +862,14 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *
return nil, fmt.Errorf("no root website found, please add a website with path /")
}
+ // ensure there /api path is not used
+ _, found = lo.Find(websites, func(website Website) bool {
+ return strings.TrimSuffix(website.path, "/") == "/api"
+ })
+ if found {
+ return nil, fmt.Errorf("path /api is reserved for API rewrites to APIs, please use a different path")
+ }
+
if len(siteDuplicates) > 0 {
duplicatePaths := lo.Map(siteDuplicates, func(website Website, i int) string {
return website.path
From 6f4cf0e362e50a05c0e5432e913e41875a9dc27e Mon Sep 17 00:00:00 2001
From: David Moore
Date: Wed, 5 Mar 2025 14:04:39 +1100
Subject: [PATCH 26/31] update go mod
---
go.mod | 21 ++++++++++-----------
go.sum | 46 ++++++++++++++++++++--------------------------
2 files changed, 30 insertions(+), 37 deletions(-)
diff --git a/go.mod b/go.mod
index 71d2aaf5e..4061bf548 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
github.com/hashicorp/consul/sdk v0.13.0
github.com/hashicorp/go-getter v1.6.2
github.com/hashicorp/go-version v1.7.0
- github.com/nitrictech/nitric/core v0.0.0-20250226055017-c8d108e2f62f
+ github.com/nitrictech/nitric/core v0.0.0-20250305021715-06f92b813ce3
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.8.1
github.com/valyala/fasthttp v1.55.0
@@ -34,7 +34,7 @@ require (
)
require (
- github.com/Masterminds/semver/v3 v3.3.0
+ github.com/Masterminds/semver/v3 v3.3.1
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
github.com/charmbracelet/bubbles v0.16.1
github.com/charmbracelet/bubbletea v0.24.2
@@ -53,7 +53,7 @@ require (
github.com/samber/lo v1.38.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/afero v1.11.0
- github.com/stretchr/testify v1.9.0
+ github.com/stretchr/testify v1.10.0
github.com/wk8/go-ordered-map/v2 v2.1.8
go.etcd.io/bbolt v1.3.6
golang.org/x/sync v0.10.0
@@ -275,18 +275,17 @@ require (
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/sdk v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
- go.uber.org/atomic v1.10.0 // indirect
go.uber.org/automaxprocs v1.5.3 // indirect
- go.uber.org/multierr v1.8.0 // indirect
- go.uber.org/zap v1.24.0 // indirect
- golang.org/x/crypto v0.31.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ go.uber.org/zap v1.27.0 // indirect
+ golang.org/x/crypto v0.32.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect
- golang.org/x/net v0.33.0 // indirect
- golang.org/x/sys v0.28.0 // indirect
- golang.org/x/term v0.27.0 // indirect
+ golang.org/x/net v0.34.0 // indirect
+ golang.org/x/sys v0.29.0 // indirect
+ golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.6.0 // indirect
- golang.org/x/tools v0.27.0 // indirect
+ golang.org/x/tools v0.28.0 // indirect
google.golang.org/api v0.196.0 // indirect
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
diff --git a/go.sum b/go.sum
index 3f4d05cc5..923cef0df 100644
--- a/go.sum
+++ b/go.sum
@@ -73,8 +73,8 @@ github.com/Djarvur/go-err113 v0.1.0 h1:uCRZZOdMQ0TZPHYTdYpoC0bLYJKPEHPUJ8MeAa51l
github.com/Djarvur/go-err113 v0.1.0/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 h1:/fTUt5vmbkAcMBt4YQiuC23cV0kEsN1MVMNqeOW43cU=
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0/go.mod h1:ONJg5sxcbsdQQ4pOW8TGdTidT2TMAUy/2Xhr8mrYaao=
-github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
-github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
+github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA=
@@ -117,8 +117,6 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
-github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
-github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -585,8 +583,8 @@ github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm
github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
github.com/nitrictech/nitric/cloud/common v0.0.0-20241003062412-76ea6275fb0b h1:wZeUrnmhYjdhSuL6ov+kVfuFJC9H14sk0kzEpt6aRoo=
github.com/nitrictech/nitric/cloud/common v0.0.0-20241003062412-76ea6275fb0b/go.mod h1:ZsCdb3xbukhXAp9ZNbV6qWJqRC+eLkxhXy8bhs/cC2A=
-github.com/nitrictech/nitric/core v0.0.0-20250226055017-c8d108e2f62f h1:Aujlui7gF5ZiUhg0Z1vZ8tymsf37g/VwDJJI2gL8hpE=
-github.com/nitrictech/nitric/core v0.0.0-20250226055017-c8d108e2f62f/go.mod h1:3kPpyO2oZEGfurDVsTh9XTg43b/4JAIbLkaktEvRF58=
+github.com/nitrictech/nitric/core v0.0.0-20250305021715-06f92b813ce3 h1:7+D0QCKgGRyefo8BoTgnbL43schzkQxm6VP2x/oiyFY=
+github.com/nitrictech/nitric/core v0.0.0-20250305021715-06f92b813ce3/go.mod h1:h1RNGBo+aYJHtQBjCzg6bdoU85OzcL6S2AvPnUd83+4=
github.com/nunnatsa/ginkgolinter v0.16.2 h1:8iLqHIZvN4fTLDC0Ke9tbSZVcyVHoBs0HIbnVSxfHJk=
github.com/nunnatsa/ginkgolinter v0.16.2/go.mod h1:4tWRinDN1FeJgU+iJANW/kz7xKN5nYRAOfJDQUS9dOQ=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
@@ -741,8 +739,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM=
@@ -832,17 +830,14 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
-go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
-go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
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.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
-go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
-go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
-go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
+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/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -851,8 +846,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -940,8 +935,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
-golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1026,16 +1021,16 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
+golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1115,8 +1110,8 @@ golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
-golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
+golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
+golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1232,7 +1227,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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.0/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=
From fadf8005a4bbbf6fe13ec90f3a091cf9dbc7e93b Mon Sep 17 00:00:00 2001
From: David Moore
Date: Wed, 5 Mar 2025 14:16:13 +1100
Subject: [PATCH 27/31] update websites node title to CDN
---
.../frontend/src/components/architecture/nodes/WebsitesNode.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/dashboard/frontend/src/components/architecture/nodes/WebsitesNode.tsx b/pkg/dashboard/frontend/src/components/architecture/nodes/WebsitesNode.tsx
index 1965af439..7ee2e1e5d 100644
--- a/pkg/dashboard/frontend/src/components/architecture/nodes/WebsitesNode.tsx
+++ b/pkg/dashboard/frontend/src/components/architecture/nodes/WebsitesNode.tsx
@@ -24,7 +24,7 @@ export const WebsitesNode: ComponentType> = (
Date: Wed, 5 Mar 2025 14:23:48 +1100
Subject: [PATCH 28/31] fix duplicate code
---
cmd/start.go | 42 ++++++++++++++++++------------------------
1 file changed, 18 insertions(+), 24 deletions(-)
diff --git a/cmd/start.go b/cmd/start.go
index eff736c93..e699ca8ac 100644
--- a/cmd/start.go
+++ b/cmd/start.go
@@ -122,6 +122,21 @@ func createTlsCredentialsIfNotPresent(fs afero.Fs, projectDir string) {
}
}
+func runInGoroutine(
+ fn func(*cloud.LocalCloud, <-chan bool, chan<- project.ServiceRunUpdate, map[string]string) error,
+ localCloud *cloud.LocalCloud,
+ stopChan chan bool,
+ updatesChan chan project.ServiceRunUpdate,
+ localEnv map[string]string,
+) {
+ go func() {
+ if err := fn(localCloud, stopChan, updatesChan, localEnv); err != nil {
+ localCloud.Stop()
+ tui.CheckErr(err)
+ }
+ }()
+}
+
var startCmd = &cobra.Command{
Use: "start",
Short: "Run nitric services locally for development and testing",
@@ -221,32 +236,11 @@ var startCmd = &cobra.Command{
}
}()
- go func() {
- err := proj.RunServicesWithCommand(localCloud, stopChan, updatesChan, localEnv)
- if err != nil {
- localCloud.Stop()
- tui.CheckErr(err)
- }
- }()
- // FIXME: Duplicate code
- go func() {
- err := proj.RunBatchesWithCommand(localCloud, stopChan, updatesChan, localEnv)
- if err != nil {
- localCloud.Stop()
-
- tui.CheckErr(err)
- }
- }()
+ runInGoroutine(proj.RunServicesWithCommand, localCloud, stopChan, updatesChan, localEnv)
- // FIXME: Duplicate code
- go func() {
- err := proj.RunWebsitesWithCommand(localCloud, stopChan, updatesChan, localEnv)
- if err != nil {
- localCloud.Stop()
+ runInGoroutine(proj.RunBatchesWithCommand, localCloud, stopChan, updatesChan, localEnv)
- tui.CheckErr(err)
- }
- }()
+ runInGoroutine(proj.RunWebsitesWithCommand, localCloud, stopChan, updatesChan, localEnv)
go func() {
err := proj.RunWebsites(localCloud)
From 519603517d5634eec84db75dc0770cd520bb700e Mon Sep 17 00:00:00 2001
From: David Moore
Date: Wed, 5 Mar 2025 14:42:20 +1100
Subject: [PATCH 29/31] cater for website names if in root directory
---
pkg/project/project.go | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/pkg/project/project.go b/pkg/project/project.go
index e4cac7836..75caca047 100644
--- a/pkg/project/project.go
+++ b/pkg/project/project.go
@@ -832,9 +832,14 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *
return nil, fmt.Errorf("invalid error page %s, must end with .html", websiteSpec.ErrorPage)
}
- projectRelativeWebsiteFolder := filepath.Join(projectConfig.Directory, websiteSpec.GetBasedir())
+ // Get the absolute path directly
+ absBaseDir, err := filepath.Abs(filepath.Join(projectConfig.Directory, websiteSpec.GetBasedir()))
+ if err != nil {
+ return nil, fmt.Errorf("unable to get absolute path for website %s: %w", websiteSpec.GetBasedir(), err)
+ }
- websiteName := fmt.Sprintf("websites_%s", strings.ToLower(projectRelativeWebsiteFolder))
+ // Generate the website name from the base directory
+ websiteName := fmt.Sprintf("websites_%s", strings.ToLower(filepath.Base(absBaseDir)))
websites = append(websites, Website{
Name: websiteName,
From b9ed04b46ebc46ff86b315e978e13546dd8ec941 Mon Sep 17 00:00:00 2001
From: David Moore
Date: Wed, 5 Mar 2025 14:58:53 +1100
Subject: [PATCH 30/31] update title to CDN
---
.../frontend/src/lib/utils/generate-architecture-data.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/dashboard/frontend/src/lib/utils/generate-architecture-data.ts b/pkg/dashboard/frontend/src/lib/utils/generate-architecture-data.ts
index e26e79a3b..571cdfe63 100644
--- a/pkg/dashboard/frontend/src/lib/utils/generate-architecture-data.ts
+++ b/pkg/dashboard/frontend/src/lib/utils/generate-architecture-data.ts
@@ -584,7 +584,7 @@ export function generateArchitectureData(data: WebSocketResponse): {
},
'websites',
{
- title: 'websites',
+ title: 'CDN',
resource: data.websites,
icon: WindowIcon,
},
From f0d1e867561b348bfa7e1080b6a5f163d237214e Mon Sep 17 00:00:00 2001
From: David Moore
Date: Wed, 5 Mar 2025 15:09:40 +1100
Subject: [PATCH 31/31] fix arch test
---
pkg/dashboard/frontend/cypress/e2e/architecture.cy.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/dashboard/frontend/cypress/e2e/architecture.cy.ts b/pkg/dashboard/frontend/cypress/e2e/architecture.cy.ts
index 6e5064584..9381463f3 100644
--- a/pkg/dashboard/frontend/cypress/e2e/architecture.cy.ts
+++ b/pkg/dashboard/frontend/cypress/e2e/architecture.cy.ts
@@ -19,7 +19,7 @@ const expectedNodes = [
'services/my-test-secret.ts',
'my-first-secret',
'my-second-secret',
- 'websites',
+ 'CDN',
]
describe('Architecture Spec', () => {