Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions cmd/gowdk/dev_loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
84 changes: 84 additions & 0 deletions cmd/gowdk/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
<main>Home</main>
}
`)
writeCLIFile(t, about, `package app

page about
route "/about"

view {
<main>Stable</main>
}
`)
writeCLIFile(t, layout, `package app

view {
<section><Brand /><slot /></section>
}
`)
writeCLIFile(t, component, `package app

component Brand

view {
<strong>Before</strong>
}
`)

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 {
<strong>After</strong>
}
`)
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), "<strong>After</strong>") {
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")
Expand Down
10 changes: 9 additions & 1 deletion internal/buildgen/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
Expand All @@ -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")))
Expand All @@ -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)),
Expand Down Expand Up @@ -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)
}

Expand Down
69 changes: 69 additions & 0 deletions internal/buildgen/incremental_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package buildgen

import (
"encoding/json"
"os"
"path/filepath"
"strings"
Expand All @@ -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) {
Expand Down Expand Up @@ -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: `<main><Hero /></main>`,
},
}},
Components: []gwdkir.Component{{
Package: "components",
Source: "components/hero.cmp.gwdk",
Name: "Hero",
Blocks: gwdkir.Blocks{
View: true,
ViewBody: `<section>Hero</section>`,
},
}},
}
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")
Expand Down