diff --git a/cmd/gowdk/dev_loop.go b/cmd/gowdk/dev_loop.go
index 72504b2..eb51ebd 100644
--- a/cmd/gowdk/dev_loop.go
+++ b/cmd/gowdk/dev_loop.go
@@ -247,7 +247,7 @@ func newIncrementalDependencyIndex(app gwdkanalysis.Sources) (incrementalDepende
index.layoutsBySource[abs] = key
}
for _, page := range app.Pages {
- for key := range pageComponentDependencies(page, componentsByKey) {
+ for key := range pageComponentDependencies(page, componentsByKey, layoutsByKey) {
index.pagesByComponent[key] = append(index.pagesByComponent[key], page.Source)
}
for key := range pageLayoutDependencies(page, layoutsByKey) {
@@ -259,18 +259,41 @@ func newIncrementalDependencyIndex(app gwdkanalysis.Sources) (incrementalDepende
return index, true
}
-func pageComponentDependencies(page gwdkir.Page, components map[string]gwdkir.Component) map[string]bool {
+func pageComponentDependencies(page gwdkir.Page, components map[string]gwdkir.Component, layouts map[string]gwdkir.Layout) map[string]bool {
seen := map[string]bool{}
- refs, err := view.ComponentReferences(page.Blocks.ViewBody)
+ collectComponentDependenciesFromView(page.Package, page.Uses, page.Blocks.ViewBody, components, seen)
+ for _, ref := range page.Layouts {
+ if layout, ok := resolvePageLayoutDependency(page.Package, page.Uses, ref, layouts); ok {
+ collectLayoutComponentDependencies(layout, layouts, components, map[string]bool{}, seen)
+ }
+ }
+ return seen
+}
+
+func collectComponentDependenciesFromView(ownerPackage string, uses []gwdkir.Use, viewBody string, components map[string]gwdkir.Component, seen map[string]bool) {
+ refs, err := view.ComponentReferences(viewBody)
if err != nil {
- return seen
+ return
}
for _, ref := range refs {
- if component, ok := resolveComponentRef(page.Package, page.Uses, ref, components); ok {
+ if component, ok := resolveComponentRef(ownerPackage, uses, ref, components); ok {
collectComponentDependencies(component, components, seen)
}
}
- return seen
+}
+
+func collectLayoutComponentDependencies(layout gwdkir.Layout, layouts map[string]gwdkir.Layout, components map[string]gwdkir.Component, seenLayouts map[string]bool, seenComponents map[string]bool) {
+ key := sourceLayoutKey(layout.Package, layout.ID)
+ if seenLayouts[key] {
+ return
+ }
+ seenLayouts[key] = true
+ collectComponentDependenciesFromView(layout.Package, layout.Uses, layout.Blocks.ViewBody, components, seenComponents)
+ for _, ref := range layout.Layouts {
+ if parent, ok := resolveLayoutDependency(layout.Package, layout.Uses, ref, layouts); ok {
+ collectLayoutComponentDependencies(parent, layouts, components, seenLayouts, seenComponents)
+ }
+ }
}
func collectComponentDependencies(component gwdkir.Component, components map[string]gwdkir.Component, seen map[string]bool) {
diff --git a/cmd/gowdk/main_test.go b/cmd/gowdk/main_test.go
index 3c97dc9..b7e0968 100644
--- a/cmd/gowdk/main_test.go
+++ b/cmd/gowdk/main_test.go
@@ -1920,6 +1920,90 @@ view {
}
}
+func TestBuildIncrementalSPAUsesLayoutComponentDependencies(t *testing.T) {
+ root := t.TempDir()
+ page := filepath.Join(root, "home.page.gwdk")
+ about := filepath.Join(root, "about.page.gwdk")
+ layout := filepath.Join(root, "root.layout.gwdk")
+ component := filepath.Join(root, "brand.cmp.gwdk")
+ outputDir := filepath.Join(root, "dist")
+ config := writeMinimalCLIConfig(t, root)
+ writeCLIFile(t, page, `package app
+
+page home
+route "/"
+layout root
+
+view {
+ Home
+}
+`)
+ writeCLIFile(t, about, `package app
+
+page about
+route "/about"
+
+view {
+ Stable
+}
+`)
+ writeCLIFile(t, layout, `package app
+
+view {
+
+}
+`)
+ writeCLIFile(t, component, `package app
+
+component Brand
+
+view {
+ Before
+}
+`)
+
+ args := []string{"--config", config, "--out", outputDir, page, about, layout, component}
+ if err := build(args); err != nil {
+ t.Fatal(err)
+ }
+ aboutPath := filepath.Join(outputDir, "about", "index.html")
+ aboutInfo, err := os.Stat(aboutPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ time.Sleep(20 * time.Millisecond)
+ writeCLIFile(t, component, `package app
+
+component Brand
+
+view {
+ After
+}
+`)
+ used, err := buildIncrementalSPA(args, inputChange{Changed: []string{component}})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !used {
+ t.Fatal("expected incremental spa build to handle layout-only component dependency change")
+ }
+ homePayload, err := os.ReadFile(filepath.Join(outputDir, "index.html"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(homePayload), "After") {
+ t.Fatalf("expected changed layout component output:\n%s", homePayload)
+ }
+ afterAboutInfo, err := os.Stat(aboutPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !afterAboutInfo.ModTime().Equal(aboutInfo.ModTime()) {
+ t.Fatalf("expected unchanged about output mod time: before=%s after=%s", aboutInfo.ModTime(), afterAboutInfo.ModTime())
+ }
+}
+
func TestBuildIncrementalSPAUsesLayoutDependencies(t *testing.T) {
root := t.TempDir()
page := filepath.Join(root, "home.page.gwdk")
diff --git a/internal/buildgen/build.go b/internal/buildgen/build.go
index 834c63f..511c2af 100644
--- a/internal/buildgen/build.go
+++ b/internal/buildgen/build.go
@@ -404,6 +404,7 @@ func buildIncrementalFromIR(config gowdk.Config, ir gwdkir.Program, outputDir st
components, componentFailures := buildComponents(ir.Components)
layouts, layoutFailures := buildLayouts(ir.Layouts)
css, cssFailures := planCSS(config, ir, outputDir)
+ componentAssets, componentAssetFailures := planComponentFileAssets(ir.Assets, outputDir)
scopedJS, scopedJSFailures := planScopedJSAssets(ir.Assets, outputDir)
baseStylesheets := append([]gowdk.Stylesheet{}, config.Build.Stylesheets...)
baseStylesheets = append(baseStylesheets, css.stylesheets...)
@@ -412,6 +413,7 @@ func buildIncrementalFromIR(config gowdk.Config, ir gwdkir.Program, outputDir st
failures = append(failures, componentFailures...)
failures = append(failures, layoutFailures...)
failures = append(failures, cssFailures...)
+ failures = append(failures, componentAssetFailures...)
failures = append(failures, scopedJSFailures...)
if len(failures) > 0 {
return Result{}, reporter.fail("plan", errors.New(strings.Join(failures, "\n")))
@@ -420,7 +422,7 @@ func buildIncrementalFromIR(config gowdk.Config, ir gwdkir.Program, outputDir st
if err != nil {
return Result{}, reporter.fail("plan", err)
}
- runtime = append(scopedJS, runtime...)
+ runtime = append(componentAssets, append(scopedJS, runtime...)...)
reporter.info("plan", "artifacts_planned", "incremental artifacts planned", BuildEvent{
Data: map[string]string{
"css": fmt.Sprint(len(css.assets)),
@@ -457,6 +459,12 @@ func buildIncrementalFromIR(config gowdk.Config, ir gwdkir.Program, outputDir st
}
recordWriteStat(&result, wrote)
reporter.debug("write", "asset_written", "runtime asset written", BuildEvent{Path: eventPath(outputDir, artifact.Path)})
+ if artifact.AssetArtifact.Hash == "" {
+ artifact.AssetArtifact.Hash = contentHash(artifact.contents)
+ }
+ if artifact.AssetArtifact.CachePolicy == "" {
+ artifact.AssetArtifact.CachePolicy = noCacheAssetCachePolicy
+ }
result.AssetArtifacts = append(result.AssetArtifacts, artifact.AssetArtifact)
}
diff --git a/internal/buildgen/incremental_test.go b/internal/buildgen/incremental_test.go
index c542aeb..f28178d 100644
--- a/internal/buildgen/incremental_test.go
+++ b/internal/buildgen/incremental_test.go
@@ -1,6 +1,7 @@
package buildgen
import (
+ "encoding/json"
"os"
"path/filepath"
"strings"
@@ -10,6 +11,7 @@ import (
"github.com/cssbruno/gowdk"
"github.com/cssbruno/gowdk/internal/gwdkanalysis"
"github.com/cssbruno/gowdk/internal/gwdkir"
+ runtimeasset "github.com/cssbruno/gowdk/runtime/asset"
)
func TestBuildPreservesUnchangedArtifactModTimes(t *testing.T) {
@@ -177,6 +179,73 @@ func TestBuildIncrementalFromIRRendersChangedPageSources(t *testing.T) {
}
}
+func TestBuildIncrementalFromIREmitsComponentFileAssets(t *testing.T) {
+ root := t.TempDir()
+ t.Chdir(root)
+ if err := os.MkdirAll("components", 0o755); err != nil {
+ t.Fatal(err)
+ }
+ writeFile(t, filepath.Join(root, "components", "hero.png"), "fake image\n")
+ outputDir := filepath.Join(root, "dist")
+ pageSource := "pages/home.page.gwdk"
+ initial := gwdkanalysis.Sources{
+ Pages: []gwdkir.Page{{
+ Source: pageSource,
+ Package: "components",
+ ID: "home",
+ Route: "/",
+ Blocks: gwdkir.Blocks{
+ View: true,
+ ViewBody: ``,
+ },
+ }},
+ Components: []gwdkir.Component{{
+ Package: "components",
+ Source: "components/hero.cmp.gwdk",
+ Name: "Hero",
+ Blocks: gwdkir.Blocks{
+ View: true,
+ ViewBody: ``,
+ },
+ }},
+ }
+ if _, err := Build(gowdk.Config{}, initial, outputDir); err != nil {
+ t.Fatal(err)
+ }
+
+ changed := initial
+ changed.Components[0].Assets = []string{"./hero.png"}
+ result, err := BuildIncrementalFromIR(gowdk.Config{}, gwdkanalysis.BuildProgram(gowdk.Config{}, changed), outputDir, []string{pageSource})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ logicalPath := "assets/gowdk/components/components/Hero/hero.png"
+ artifact := assetArtifactByLogicalPath(t, result.AssetArtifacts, logicalPath)
+ emittedRel := filepath.ToSlash(mustRelativePath(t, outputDir, artifact.Path))
+ if emittedRel == logicalPath || !strings.HasPrefix(emittedRel, "assets/gowdk/components/components/Hero/hero.") || !strings.HasSuffix(emittedRel, ".png") {
+ t.Fatalf("expected content-hashed incremental component asset filename, got %q", emittedRel)
+ }
+ if got := readFile(t, artifact.Path); got != "fake image\n" {
+ t.Fatalf("unexpected emitted asset contents: %q", got)
+ }
+
+ var assets runtimeasset.Manifest
+ manifestPayload := readBytes(t, filepath.Join(outputDir, assetManifestFile))
+ if err := json.Unmarshal(manifestPayload, &assets); err != nil {
+ t.Fatal(err)
+ }
+ if assets.Resolve(logicalPath) != emittedRel {
+ t.Fatalf("expected component asset manifest mapping, got %s", manifestPayload)
+ }
+ if hash := assets.Hash(logicalPath); !strings.HasPrefix(hash, "sha256:") {
+ t.Fatalf("expected component asset hash, got %q in %s", hash, manifestPayload)
+ }
+ if policy := assets.CachePolicy(logicalPath); policy != immutableAssetCachePolicy {
+ t.Fatalf("expected immutable component asset cache policy, got %q", policy)
+ }
+}
+
func TestBuildIncrementalRemovesStaleChangedPageRouteOutput(t *testing.T) {
outputDir := t.TempDir()
source := filepath.Join(t.TempDir(), "home.page.gwdk")