diff --git a/cmd/gowdk/dev_loop.go b/cmd/gowdk/dev_loop.go
index 3b0cd1b..72504b2 100644
--- a/cmd/gowdk/dev_loop.go
+++ b/cmd/gowdk/dev_loop.go
@@ -16,6 +16,7 @@ import (
"github.com/cssbruno/gowdk/internal/gwdkanalysis"
"github.com/cssbruno/gowdk/internal/gwdkir"
"github.com/cssbruno/gowdk/internal/lang"
+ "github.com/cssbruno/gowdk/internal/view"
)
func buildDevChange(args []string, change inputChange, allowIncremental bool) (bool, error) {
@@ -37,6 +38,7 @@ func buildIncrementalSPA(args []string, change inputChange) (bool, error) {
if err != nil {
return true, err
}
+ timings := newBuildTimingRecorder(plan.Timings)
if plan.shouldBuildConfiguredTargets() {
return false, nil
}
@@ -67,7 +69,13 @@ func buildIncrementalSPA(args []string, change inputChange) (bool, error) {
paths = discovered
}
- app, diagnostics := lang.ParseBuildFiles(paths)
+ timings.counter("incremental_input_changes", len(change.Changed))
+ var app gwdkanalysis.Sources
+ var diagnostics lang.Diagnostics
+ timings.measure("parse_lower", func() error {
+ app, diagnostics = lang.ParseBuildFiles(paths)
+ return nil
+ })
for _, diagnostic := range diagnostics {
fmt.Fprintln(os.Stderr, diagnostic.String())
}
@@ -75,18 +83,32 @@ func buildIncrementalSPA(args []string, change inputChange) (bool, error) {
return true, fmt.Errorf("build failed")
}
- pageSources, incremental := changedPageSources(app, change.Changed)
+ incrementalPlan, incremental := changedIncrementalSPAPages(app, change.Changed)
if !incremental {
return false, nil
}
- ir := gwdkanalysis.BuildProgram(options.Config, app)
- result, err := buildgen.BuildIncrementalFromIR(options.Config, ir, outputDir, pageSources)
- if err != nil {
+ timings.counter("incremental_page_changes", incrementalPlan.PageChanges)
+ timings.counter("incremental_component_changes", incrementalPlan.ComponentChanges)
+ timings.counter("incremental_layout_changes", incrementalPlan.LayoutChanges)
+ timings.counter("incremental_affected_pages", len(incrementalPlan.PageSources))
+ var ir gwdkir.Program
+ timings.measure("ir_assembly", func() error {
+ ir = gwdkanalysis.BuildProgram(options.Config, app)
+ return nil
+ })
+ var result buildgen.Result
+ if err := timings.measure("output_plan_writes", func() error {
+ var buildErr error
+ result, buildErr = buildgen.BuildIncrementalFromIR(options.Config, ir, outputDir, incrementalPlan.PageSources)
+ return buildErr
+ }); err != nil {
printBuildgenBuildErrorReport(err, options.Debug)
return true, err
}
+ timings.counter("files_written", result.WriteStats.FilesWritten)
+ timings.counter("identical_writes_skipped", result.WriteStats.IdenticalWritesSkipped)
for _, artifact := range result.Artifacts {
- if pageIDChanged(artifact.PageID, pageSources, app.Pages) {
+ if pageIDChanged(artifact.PageID, incrementalPlan.PageSources, app.Pages) {
fmt.Println(artifact.Path)
}
}
@@ -106,6 +128,9 @@ func buildIncrementalSPA(args []string, change inputChange) (bool, error) {
fmt.Println(result.BuildReportPath)
}
printBuildgenBuildReport(result.Report, options.Debug)
+ if _, err := timings.write(outputDir, plan.TimingsPath); err != nil {
+ return true, err
+ }
return true, nil
}
@@ -134,28 +159,224 @@ func devConfigPath(configPath string) (string, bool) {
return filepath.Clean(abs), err == nil
}
-func changedPageSources(app gwdkanalysis.Sources, changedPaths []string) ([]string, bool) {
- pageSources := map[string]string{}
+type incrementalSPAChangePlan struct {
+ PageSources []string
+ PageChanges int
+ ComponentChanges int
+ LayoutChanges int
+}
+
+func changedIncrementalSPAPages(app gwdkanalysis.Sources, changedPaths []string) (incrementalSPAChangePlan, bool) {
+ index, ok := newIncrementalDependencyIndex(app)
+ if !ok {
+ return incrementalSPAChangePlan{}, false
+ }
+ affected := map[string]bool{}
+ plan := incrementalSPAChangePlan{}
+ for _, changedPath := range changedPaths {
+ abs, ok := cleanAbs(changedPath)
+ if !ok {
+ return incrementalSPAChangePlan{}, false
+ }
+ if source, ok := index.pagesBySource[abs]; ok {
+ affected[source] = true
+ plan.PageChanges++
+ continue
+ }
+ if key, ok := index.componentsBySource[abs]; ok {
+ for _, source := range index.pagesByComponent[key] {
+ affected[source] = true
+ }
+ plan.ComponentChanges++
+ continue
+ }
+ if key, ok := index.layoutsBySource[abs]; ok {
+ for _, source := range index.pagesByLayout[key] {
+ affected[source] = true
+ }
+ plan.LayoutChanges++
+ continue
+ }
+ return incrementalSPAChangePlan{}, false
+ }
+ plan.PageSources = sortedKeys(affected)
+ return plan, true
+}
+
+type incrementalDependencyIndex struct {
+ pagesBySource map[string]string
+ componentsBySource map[string]string
+ layoutsBySource map[string]string
+ pagesByComponent map[string][]string
+ pagesByLayout map[string][]string
+}
+
+func newIncrementalDependencyIndex(app gwdkanalysis.Sources) (incrementalDependencyIndex, bool) {
+ index := incrementalDependencyIndex{
+ pagesBySource: map[string]string{},
+ componentsBySource: map[string]string{},
+ layoutsBySource: map[string]string{},
+ pagesByComponent: map[string][]string{},
+ pagesByLayout: map[string][]string{},
+ }
+ componentsByKey := map[string]gwdkir.Component{}
for _, page := range app.Pages {
abs, ok := cleanAbs(page.Source)
- if ok {
- pageSources[abs] = page.Source
+ if !ok {
+ return incrementalDependencyIndex{}, false
}
+ index.pagesBySource[abs] = page.Source
}
-
- var changedPages []string
- for _, changedPath := range changedPaths {
- abs, ok := cleanAbs(changedPath)
+ for _, component := range app.Components {
+ key := sourceComponentKey(component.Package, component.Name)
+ componentsByKey[key] = component
+ abs, ok := cleanAbs(component.Source)
if !ok {
- return nil, false
+ return incrementalDependencyIndex{}, false
}
- source, ok := pageSources[abs]
+ index.componentsBySource[abs] = key
+ }
+ layoutsByKey := map[string]gwdkir.Layout{}
+ for _, layout := range app.Layouts {
+ key := sourceLayoutKey(layout.Package, layout.ID)
+ layoutsByKey[key] = layout
+ abs, ok := cleanAbs(layout.Source)
if !ok {
- return nil, false
+ return incrementalDependencyIndex{}, false
}
- changedPages = append(changedPages, source)
+ index.layoutsBySource[abs] = key
+ }
+ for _, page := range app.Pages {
+ for key := range pageComponentDependencies(page, componentsByKey) {
+ index.pagesByComponent[key] = append(index.pagesByComponent[key], page.Source)
+ }
+ for key := range pageLayoutDependencies(page, layoutsByKey) {
+ index.pagesByLayout[key] = append(index.pagesByLayout[key], page.Source)
+ }
+ }
+ sortDependencyIndex(index.pagesByComponent)
+ sortDependencyIndex(index.pagesByLayout)
+ return index, true
+}
+
+func pageComponentDependencies(page gwdkir.Page, components map[string]gwdkir.Component) map[string]bool {
+ seen := map[string]bool{}
+ refs, err := view.ComponentReferences(page.Blocks.ViewBody)
+ if err != nil {
+ return seen
+ }
+ for _, ref := range refs {
+ if component, ok := resolveComponentRef(page.Package, page.Uses, ref, components); ok {
+ collectComponentDependencies(component, components, seen)
+ }
+ }
+ return seen
+}
+
+func collectComponentDependencies(component gwdkir.Component, components map[string]gwdkir.Component, seen map[string]bool) {
+ key := sourceComponentKey(component.Package, component.Name)
+ if seen[key] {
+ return
+ }
+ seen[key] = true
+ refs, err := view.ComponentReferences(component.Blocks.ViewBody)
+ if err != nil {
+ return
+ }
+ for _, ref := range refs {
+ if child, ok := resolveComponentRef(component.Package, component.Uses, ref, components); ok {
+ collectComponentDependencies(child, components, seen)
+ }
+ }
+}
+
+func resolveComponentRef(ownerPackage string, uses []gwdkir.Use, ref string, components map[string]gwdkir.Component) (gwdkir.Component, bool) {
+ if alias, name, ok := strings.Cut(ref, "."); ok {
+ for _, use := range uses {
+ if use.Alias == alias {
+ component, exists := components[sourceComponentKey(use.Package, name)]
+ return component, exists
+ }
+ }
+ return gwdkir.Component{}, false
+ }
+ if ownerPackage != "" {
+ if component, ok := components[sourceComponentKey(ownerPackage, ref)]; ok {
+ return component, true
+ }
+ }
+ component, ok := components[sourceComponentKey("", ref)]
+ return component, ok
+}
+
+func pageLayoutDependencies(page gwdkir.Page, layouts map[string]gwdkir.Layout) map[string]bool {
+ seen := map[string]bool{}
+ for _, ref := range page.Layouts {
+ if layout, ok := resolvePageLayoutDependency(page.Package, page.Uses, ref, layouts); ok {
+ collectLayoutDependencies(layout, layouts, seen)
+ }
+ }
+ return seen
+}
+
+func collectLayoutDependencies(layout gwdkir.Layout, layouts map[string]gwdkir.Layout, seen map[string]bool) {
+ key := sourceLayoutKey(layout.Package, layout.ID)
+ if seen[key] {
+ return
+ }
+ seen[key] = true
+ for _, ref := range layout.Layouts {
+ if parent, ok := resolveLayoutDependency(layout.Package, layout.Uses, ref, layouts); ok {
+ collectLayoutDependencies(parent, layouts, seen)
+ }
+ }
+}
+
+func resolvePageLayoutDependency(ownerPackage string, uses []gwdkir.Use, ref string, layouts map[string]gwdkir.Layout) (gwdkir.Layout, bool) {
+ return resolveLayoutDependency(ownerPackage, uses, ref, layouts)
+}
+
+func resolveLayoutDependency(ownerPackage string, uses []gwdkir.Use, ref string, layouts map[string]gwdkir.Layout) (gwdkir.Layout, bool) {
+ if alias, id, ok := strings.Cut(ref, "."); ok {
+ for _, use := range uses {
+ if use.Alias == alias {
+ layout, exists := layouts[sourceLayoutKey(use.Package, id)]
+ return layout, exists
+ }
+ }
+ return gwdkir.Layout{}, false
+ }
+ if ownerPackage != "" {
+ if layout, ok := layouts[sourceLayoutKey(ownerPackage, ref)]; ok {
+ return layout, true
+ }
+ }
+ layout, ok := layouts[sourceLayoutKey("", ref)]
+ return layout, ok
+}
+
+func sourceComponentKey(packageName string, name string) string {
+ return packageName + "\x00" + name
+}
+
+func sourceLayoutKey(packageName string, id string) string {
+ return packageName + "\x00" + id
+}
+
+func sortedKeys(values map[string]bool) []string {
+ keys := make([]string, 0, len(values))
+ for key := range values {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+ return keys
+}
+
+func sortDependencyIndex(index map[string][]string) {
+ for key, values := range index {
+ sort.Strings(values)
+ index[key] = values
}
- return changedPages, len(changedPages) > 0
}
func pageIDChanged(pageID string, changedSources []string, pages []gwdkir.Page) bool {
diff --git a/cmd/gowdk/main_test.go b/cmd/gowdk/main_test.go
index af92700..3c97dc9 100644
--- a/cmd/gowdk/main_test.go
+++ b/cmd/gowdk/main_test.go
@@ -1822,9 +1822,10 @@ view {
}
}
-func TestBuildIncrementalSPAFallsBackForComponentChanges(t *testing.T) {
+func TestBuildIncrementalSPAUsesComponentDependencies(t *testing.T) {
root := t.TempDir()
page := filepath.Join(root, "home.page.gwdk")
+ about := filepath.Join(root, "about.page.gwdk")
component := filepath.Join(root, "hero.cmp.gwdk")
outputDir := filepath.Join(root, "dist")
config := writeMinimalCLIConfig(t, root)
@@ -1836,6 +1837,15 @@ route "/"
view {
}
+`)
+ writeCLIFile(t, about, `package app
+
+page about
+route "/about"
+
+view {
+ Stable
+}
`)
writeCLIFile(t, component, `package app
@@ -1850,12 +1860,160 @@ view {
}
`)
- used, err := buildIncrementalSPA([]string{"--config", config, "--out", outputDir, page, component}, inputChange{Changed: []string{component}})
+ args := []string{"--config", config, "--timings", "--out", outputDir, page, about, 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 Hero
+
+props {
+ title string
+}
+
+view {
+
{title} 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 component dependency change")
+ }
+ homePayload, err := os.ReadFile(filepath.Join(outputDir, "index.html"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(homePayload), "GOWDK after") {
+ t.Fatalf("expected changed 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())
+ }
+ payload, err := os.ReadFile(filepath.Join(outputDir, buildTimingsFile))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var timings buildTimingReport
+ if err := json.Unmarshal(payload, &timings); err != nil {
+ t.Fatalf("invalid timings JSON: %v\n%s", err, payload)
+ }
+ if timings.Counters["incremental_component_changes"] != 1 || timings.Counters["incremental_affected_pages"] != 1 {
+ t.Fatalf("expected incremental dependency counters, got %#v", timings.Counters)
+ }
+ if _, ok := timings.Counters["files_written"]; !ok {
+ t.Fatalf("expected incremental write counters, got %#v", timings.Counters)
+ }
+}
+
+func TestBuildIncrementalSPAUsesLayoutDependencies(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")
+ 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 {
+
+}
+`)
+
+ args := []string{"--config", config, "--out", outputDir, page, about, layout}
+ 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, layout, `package app
+
+view {
+
+}
+`)
+ used, err := buildIncrementalSPA(args, inputChange{Changed: []string{layout}})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !used {
+ t.Fatal("expected incremental spa build to handle layout dependency change")
+ }
+ homePayload, err := os.ReadFile(filepath.Join(outputDir, "index.html"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(homePayload), `class="after"`) {
+ t.Fatalf("expected changed layout 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 TestBuildIncrementalSPAFallsBackForConfigChanges(t *testing.T) {
+ root := t.TempDir()
+ page := filepath.Join(root, "home.page.gwdk")
+ outputDir := filepath.Join(root, "dist")
+ config := writeMinimalCLIConfig(t, root)
+ writeCLIFile(t, page, `package app
+
+page home
+route "/"
+
+view {
+ Home
+}
+`)
+
+ used, err := buildIncrementalSPA([]string{"--config", config, "--out", outputDir, page}, inputChange{Changed: []string{config}})
if err != nil {
t.Fatal(err)
}
if used {
- t.Fatal("expected component change to fall back to full build")
+ t.Fatal("expected config change to fall back to full build")
}
}
diff --git a/docs/compiler/README.md b/docs/compiler/README.md
index ce9c706..2a7feb3 100644
--- a/docs/compiler/README.md
+++ b/docs/compiler/README.md
@@ -46,6 +46,8 @@ Not implemented yet:
- `browser-compiler.md`: browser-facing partial runtime, JavaScript islands, and
component-level WASM island behavior.
- `build-report.md`: generated build report schema and CLI debug output.
+- `incremental-cache-keys.md`: compiler cache-key model and implemented
+ incremental invalidation slice.
- `manifest.md`: manifest and site-map JSON contracts.
- `syntax-contributors.md`: checklist for parser, diagnostics, IR, generation,
docs, and fixture coverage when syntax changes.
diff --git a/docs/compiler/build-report.md b/docs/compiler/build-report.md
index f82779d..8928350 100644
--- a/docs/compiler/build-report.md
+++ b/docs/compiler/build-report.md
@@ -92,3 +92,10 @@ Current phases include `config_load`, `source_discovery`, `parse_lower`,
`ir_assembly`, `go_binding`, `ir_validation`, `contract_validation`,
`output_plan_writes`, `app_generation`, `binary_build`, `wasm_build`,
`backend_app_generation`, and `backend_binary_build` when those paths run.
+
+`gowdk dev` incremental SPA rebuilds reuse the same sidecar when `--timings` is
+forwarded in the build flags. Incremental counters include
+`incremental_input_changes`, `incremental_page_changes`,
+`incremental_component_changes`, `incremental_layout_changes`,
+`incremental_affected_pages`, `files_written`, and
+`identical_writes_skipped`.
diff --git a/docs/compiler/incremental-cache-keys.md b/docs/compiler/incremental-cache-keys.md
new file mode 100644
index 0000000..e2e2c7f
--- /dev/null
+++ b/docs/compiler/incremental-cache-keys.md
@@ -0,0 +1,69 @@
+# Incremental Cache Keys
+
+GOWDK cache keys are deterministic compiler inputs, not runtime state. The
+cache model is split by phase so invalidation can stay local and reviewable.
+
+## Key Model
+
+Each key is a stable hash over the fields that affect the next compiler phase:
+
+- `.gwdk` source key: parsed source kind, package, declared identity, route,
+ metadata declarations, imports, `use` declarations, block bodies, endpoints,
+ component contracts, layout references, CSS/asset selections, and source file
+ path. Parser errors use the raw file hash until the file parses again.
+- Go ABI key: owning package import path, package name, exported handler/type
+ names used by `.gwdk`, resolved signatures, relevant struct fields and tags,
+ build tags, GOOS/GOARCH, and Go toolchain version. Function bodies are not ABI
+ input unless generated output embeds the body through an inline Go block.
+- Config/target key: normalized compiler config, selected modules, selected
+ build target, output mode, enabled feature flags, and active addons that affect
+ generated output.
+- Toolchain key: Go version, GOOS/GOARCH, build tags, and compiler feature
+ gates that can change package loading or generated code.
+- IR key: versioned `gwdkir.Program` records that downstream generators consume,
+ excluding diagnostics ordering noise and runtime-only secrets.
+- Output-plan key: generated route, asset, CSS, app, backend, WASM, and binary
+ plans plus the generator version that owns their shape.
+- Generated-file key: output path, content hash, cache policy, and the source
+ output-plan record that produced it.
+
+Runtime-only values do not invalidate static output. For example, CSRF secret
+values rotate at runtime and are not cache inputs unless the generated code shape
+or config field that enables CSRF changes.
+
+## Reverse Dependencies
+
+Reverse dependencies answer "which pages must be regenerated when this input
+changes?" and are derived from IR plus parsed view references:
+
+- Page source changes affect that page.
+- Component source changes affect pages that call the component directly or
+ through another component.
+- Layout source changes affect pages that name the layout, including parent
+ layout chains.
+- CSS source changes affect CSS artifacts and pages that include the stylesheet.
+- Backend binding ABI changes affect generated adapters and reports, not static
+ page HTML unless a build/load function contributes build-time output.
+- Config, target, addon, toolchain, added source, and removed source changes
+ conservatively invalidate the whole selected build.
+
+Dependencies stay attached to explicit source kinds. Avoid catch-all global
+cache state; a phase may cache local package inspection or output planning, but
+the invalidation edge must name the source kind and owner.
+
+## Implemented Slice
+
+The current implementation keeps the existing content-hash input snapshot for
+`gowdk dev`, then adds reverse dependencies for incremental SPA rebuilds:
+
+- changed page sources still render only those pages;
+- changed component sources render pages that reference the component directly
+ or transitively through another component;
+- changed layout sources render pages that use the layout or a child layout that
+ inherits from it;
+- added, removed, config, generated app, binary, WASM, backend, and configured
+ target changes still fall back to the full build path.
+
+When `--timings` is enabled, incremental rebuilds write the same timing sidecar
+as normal builds and include counters for input changes, affected pages,
+component/layout/page changes, files written, and identical writes skipped.
diff --git a/docs/reference/dev.md b/docs/reference/dev.md
index 549029a..ee41e59 100644
--- a/docs/reference/dev.md
+++ b/docs/reference/dev.md
@@ -23,20 +23,24 @@ attached to the terminal.
## Rebuild Scope
-For plain SPA `--out` builds, page-only edits use the incremental SPA renderer:
-the dev loop validates the full compiler IR, refreshes manifests, writes changed
-page output, and removes stale route output for changed pages.
+For plain SPA `--out` builds, page, component, and layout edits use the
+incremental SPA renderer when the changed files are already in the source set.
+The dev loop validates the full compiler IR, derives page/component/layout
+reverse dependencies, refreshes manifests, writes affected page output, and
+removes stale route output for changed pages.
These changes use the full build path:
-- component files;
-- layout files;
- CSS files;
- config files;
- source-set changes;
- target changes;
- generated app, binary, backend, or WASM output.
+When build flags include `--timings`, incremental rebuilds update the timings
+sidecar with counters for input changes, affected pages, component/layout/page
+changes, files written, and identical writes skipped.
+
## HMR
Component-level HMR is not part of the current contract. The P0 baseline is
diff --git a/internal/buildgen/build.go b/internal/buildgen/build.go
index 69a5194..834c63f 100644
--- a/internal/buildgen/build.go
+++ b/internal/buildgen/build.go
@@ -442,16 +442,20 @@ func buildIncrementalFromIR(config gowdk.Config, ir gwdkir.Program, outputDir st
})
changedPageIDs := map[string]bool{}
for _, artifact := range css.assets {
- if err := writeFileIfChanged(artifact.Path, artifact.contents); err != nil {
+ wrote, err := writeFileIfChangedStatus(artifact.Path, artifact.contents)
+ if err != nil {
return Result{}, reporter.fail("write", err)
}
+ recordWriteStat(&result, wrote)
reporter.debug("write", "css_written", "CSS artifact written", BuildEvent{Path: eventPath(outputDir, artifact.Path)})
result.CSSArtifacts = append(result.CSSArtifacts, artifact.CSSArtifact)
}
for _, artifact := range runtime {
- if err := writeFileIfChanged(artifact.Path, artifact.contents); err != nil {
+ wrote, err := writeFileIfChangedStatus(artifact.Path, artifact.contents)
+ if err != nil {
return Result{}, reporter.fail("write", err)
}
+ recordWriteStat(&result, wrote)
reporter.debug("write", "asset_written", "runtime asset written", BuildEvent{Path: eventPath(outputDir, artifact.Path)})
result.AssetArtifacts = append(result.AssetArtifacts, artifact.AssetArtifact)
}
@@ -492,9 +496,11 @@ func buildIncrementalFromIR(config gowdk.Config, ir gwdkir.Program, outputDir st
continue
}
for _, artifact := range pageArtifacts {
- if err := writeFileIfChanged(artifact.Path, artifact.contents); err != nil {
+ wrote, err := writeFileIfChangedStatus(artifact.Path, artifact.contents)
+ if err != nil {
return Result{}, reporter.fail("write", err)
}
+ recordWriteStat(&result, wrote)
reporter.debug("write", "page_written", "page artifact written", BuildEvent{
PageID: artifact.PageID,
Route: artifact.Route,