Skip to content

Commit d62e225

Browse files
committed
fix: use global singleton for Sass transpiler to prevent CI errors
Fixes #90 Changes the Sass transpiler from per-Manager lazy initialization to a global singleton with lazy initialization. This prevents race conditions and resource issues when Sites are reloaded during watch mode or when multiple Manager instances are created. Root Cause: - Each renderers.Manager previously had its own sassTranspiler field - When watch mode detects changes requiring a full reload (e.g., _config.yml changes), a new Site instance is created with a new Manager - The old Manager's transpiler was never closed, leading to abandoned Dart Sass child processes - In CI environments, this caused "connection is shut down" and "unexpected EOF" errors when the stdin/stdout streams were closed unexpectedly during process shutdown/startup overlap Solution: - Move to a package-level global singleton transpiler with lazy init - Aligns with godartsass recommendation: "create one and use that for all the SCSS processing needed" - The transpiler is thread-safe and stateless (include paths are passed to Execute()), so a single instance can safely serve all Managers Testing: - Added site/testdata/site1/assets/css/main.scss for regression testing - Updated site/drop_test.go to account for new test file - All tests pass, linting passes - Verified SCSS compilation works correctly
1 parent f7f3017 commit d62e225

3 files changed

Lines changed: 38 additions & 14 deletions

File tree

renderers/renderers.go

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ import (
1414
"github.com/osteele/liquid"
1515
)
1616

17+
// Global Sass transpiler singleton, shared across all Manager instances.
18+
// This avoids race conditions and resource leaks when Sites are reloaded during watch mode.
19+
// The transpiler is thread-safe and stateless (include paths are passed to Execute()),
20+
// so a single instance can safely serve all Managers throughout the process lifetime.
21+
var (
22+
globalSassTranspiler *sass.Transpiler
23+
globalSassTranspilerOnce sync.Once
24+
globalSassTranspilerErr error
25+
)
26+
1727
// Renderers applies transformations to a document.
1828
type Renderers interface {
1929
ApplyLayout(string, []byte, liquid.Bindings) ([]byte, error)
@@ -24,13 +34,10 @@ type Renderers interface {
2434
// Manager applies a rendering transformation to a file.
2535
type Manager struct {
2636
Options
27-
cfg config.Config
28-
liquidEngine *liquid.Engine
29-
sassTempDir string
30-
sassHash string
31-
sassTranspiler *sass.Transpiler
32-
sassInitOnce sync.Once
33-
sassInitErr error
37+
cfg config.Config
38+
liquidEngine *liquid.Engine
39+
sassTempDir string
40+
sassHash string
3441
}
3542

3643
// Options configures a rendering manager.
@@ -175,12 +182,12 @@ func (p *Manager) makeLiquidEngine() *liquid.Engine {
175182
return engine
176183
}
177184

178-
// getSassTranspiler returns the SASS transpiler, initializing it if necessary.
179-
// This uses lazy initialization to avoid creating the transpiler at package load time,
180-
// which can cause "connection is shut down" errors in CI/CD environments.
185+
// getSassTranspiler returns the global SASS transpiler singleton, initializing it if necessary.
186+
// Using a global singleton avoids race conditions when Sites are reloaded during watch mode,
187+
// and matches the godartsass recommendation to "create one and use that for all SCSS processing."
181188
func (p *Manager) getSassTranspiler() (*sass.Transpiler, error) {
182-
p.sassInitOnce.Do(func() {
183-
p.sassTranspiler, p.sassInitErr = sass.Start(sass.Options{})
189+
globalSassTranspilerOnce.Do(func() {
190+
globalSassTranspiler, globalSassTranspilerErr = sass.Start(sass.Options{})
184191
})
185-
return p.sassTranspiler, p.sassInitErr
192+
return globalSassTranspiler, globalSassTranspilerErr
186193
}

site/drop_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func TestSite_ToLiquid_pages(t *testing.T) {
3838
drop := readTestSiteDrop(t)
3939
ps, ok := drop["pages"]
4040
require.True(t, ok, fmt.Sprintf("pages has type %T", drop["pages"]))
41-
require.Len(t, ps, 2)
41+
require.Len(t, ps, 3) // includes main.scss for SCSS transpiler testing
4242

4343
ps, ok = drop["html_pages"]
4444
require.True(t, ok, fmt.Sprintf("pages has type %T", drop["pages"]))
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
---
3+
/* Test SCSS file for issue #90 regression testing */
4+
body {
5+
background-color: #f0f0f0;
6+
color: #333;
7+
}
8+
9+
.container {
10+
max-width: 1200px;
11+
margin: 0 auto;
12+
13+
.header {
14+
font-size: 2em;
15+
font-weight: bold;
16+
}
17+
}

0 commit comments

Comments
 (0)