Skip to content

Commit 391d2a8

Browse files
Frankie G-Jthitch97
andauthored
implement process reloading (#157)
Co-authored-by: Tim Hitchener <thitch97@users.noreply.github.com>
1 parent 24b2b77 commit 391d2a8

11 files changed

Lines changed: 415 additions & 29 deletions

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ command is generated from the contents of `package.json`. For example, given a
1818

1919
The start command will be `<prestart-command> && <start-command> && <poststart-command>`.
2020

21+
## Enabling reloadable process types
22+
23+
You can configure this buildpack to wrap the entrypoint process of your app
24+
such that it kills and restarts the process whenever files change in the app's working
25+
directory in the container. With this feature enabled, copying new
26+
verisons of source code into the running container will trigger your app's
27+
process to restart. Set the environment variable `BP_LIVE_RELOAD_ENABLED=true`
28+
at build time to enable this feature.
29+
2130
## Integration
2231

2332
This CNB sets a start command, so there's currently no scenario we can

build.go

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"os"
77
"path/filepath"
8+
"strings"
89

910
"github.com/paketo-buildpacks/packit"
1011
"github.com/paketo-buildpacks/packit/scribe"
@@ -57,21 +58,51 @@ func Build(pathParser PathParser, logger scribe.Logger) packit.BuildFunc {
5758
command = fmt.Sprintf("cd %s && %s", projectPath, command)
5859
}
5960

61+
processes := []packit.Process{
62+
{
63+
Type: "web",
64+
Command: command,
65+
},
66+
}
67+
68+
shouldReload, err := checkLiveReloadEnabled()
69+
if err != nil {
70+
return packit.BuildResult{}, err
71+
}
72+
73+
if shouldReload {
74+
processes = []packit.Process{
75+
{
76+
Type: "web",
77+
Command: strings.Join([]string{
78+
"watchexec",
79+
"--restart",
80+
fmt.Sprintf("--watch %s", filepath.Join(context.WorkingDir, projectPath)),
81+
fmt.Sprintf("--ignore %s", filepath.Join(context.WorkingDir, projectPath, "package.json")),
82+
fmt.Sprintf("--ignore %s", filepath.Join(context.WorkingDir, projectPath, "yarn.lock")),
83+
fmt.Sprintf("--ignore %s", filepath.Join(context.WorkingDir, projectPath, "node_modules")),
84+
fmt.Sprintf(`"%s"`, command),
85+
}, " "),
86+
},
87+
{
88+
Type: "no-reload",
89+
Command: command,
90+
},
91+
}
92+
}
93+
6094
logger.Process("Assigning launch processes")
61-
logger.Subprocess("web: %s", command)
95+
for _, process := range processes {
96+
logger.Subprocess("%s: %s", process.Type, process.Command)
97+
}
6298
logger.Break()
6399

64100
return packit.BuildResult{
65101
Plan: packit.BuildpackPlan{
66102
Entries: []packit.BuildpackPlanEntry{},
67103
},
68104
Launch: packit.LaunchMetadata{
69-
Processes: []packit.Process{
70-
{
71-
Type: "web",
72-
Command: command,
73-
},
74-
},
105+
Processes: processes,
75106
},
76107
}, nil
77108
}

build_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io/ioutil"
88
"os"
99
"path/filepath"
10+
"strings"
1011
"testing"
1112

1213
"github.com/paketo-buildpacks/packit"
@@ -99,6 +100,53 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
99100
Expect(pathParser.GetCall.Receives.Path).To(Equal(workingDir))
100101
})
101102

103+
context("when BP_LIVE_RELOAD_ENABLED=true in the build environment", func() {
104+
it.Before(func() {
105+
os.Setenv("BP_LIVE_RELOAD_ENABLED", "true")
106+
})
107+
108+
it.After(func() {
109+
os.Unsetenv("BP_LIVE_RELOAD_ENABLED")
110+
})
111+
112+
it("adds a reloadable start command that ignores package manager files and makes it the default", func() {
113+
result, err := build(packit.BuildContext{
114+
WorkingDir: workingDir,
115+
CNBPath: cnbDir,
116+
Stack: "some-stack",
117+
BuildpackInfo: packit.BuildpackInfo{
118+
Name: "Some Buildpack",
119+
Version: "some-version",
120+
},
121+
Plan: packit.BuildpackPlan{
122+
Entries: []packit.BuildpackPlanEntry{},
123+
},
124+
Layers: packit.Layers{Path: layersDir},
125+
})
126+
Expect(err).NotTo(HaveOccurred())
127+
128+
Expect(result.Launch.Processes).To(Equal([]packit.Process{
129+
{
130+
Type: "web",
131+
Command: strings.Join([]string{
132+
"watchexec",
133+
"--restart",
134+
fmt.Sprintf("--watch %s/some-project-dir", workingDir),
135+
fmt.Sprintf("--ignore %s/some-project-dir/package.json", workingDir),
136+
fmt.Sprintf("--ignore %s/some-project-dir/yarn.lock", workingDir),
137+
fmt.Sprintf("--ignore %s/some-project-dir/node_modules", workingDir),
138+
`"cd some-project-dir && some-prestart-command && some-start-command && some-poststart-command"`,
139+
}, " "),
140+
},
141+
{
142+
Type: "no-reload",
143+
Command: "cd some-project-dir && some-prestart-command && some-start-command && some-poststart-command",
144+
},
145+
}))
146+
Expect(pathParser.GetCall.Receives.Path).To(Equal(workingDir))
147+
})
148+
})
149+
102150
context("when the package.json does not include a prestart command", func() {
103151
it.Before(func() {
104152
err := ioutil.WriteFile(filepath.Join(workingDir, "some-project-dir", "package.json"), []byte(`{
@@ -343,5 +391,32 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
343391
Expect(err).To(MatchError("path-parser-error"))
344392
})
345393
})
394+
395+
context("when BP_LIVE_RELOAD_ENABLED is set to an invalid value", func() {
396+
it.Before(func() {
397+
os.Setenv("BP_LIVE_RELOAD_ENABLED", "not-a-bool")
398+
})
399+
400+
it.After(func() {
401+
os.Unsetenv("BP_LIVE_RELOAD_ENABLED")
402+
})
403+
404+
it("returns an error", func() {
405+
_, err := build(packit.BuildContext{
406+
WorkingDir: workingDir,
407+
CNBPath: cnbDir,
408+
Stack: "some-stack",
409+
BuildpackInfo: packit.BuildpackInfo{
410+
Name: "Some Buildpack",
411+
Version: "some-version",
412+
},
413+
Plan: packit.BuildpackPlan{
414+
Entries: []packit.BuildpackPlanEntry{},
415+
},
416+
Layers: packit.Layers{Path: layersDir},
417+
})
418+
Expect(err).To(MatchError(ContainSubstring("failed to parse BP_LIVE_RELOAD_ENABLED value not-a-bool")))
419+
})
420+
})
346421
})
347422
}

detect.go

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"strconv"
78

89
"github.com/paketo-buildpacks/packit"
910
)
@@ -28,29 +29,56 @@ func Detect(projectPathParser PathParser) packit.DetectFunc {
2829
return packit.DetectResult{}, fmt.Errorf("failed to stat yarn.lock: %w", err)
2930
}
3031

32+
requirements := []packit.BuildPlanRequirement{
33+
{
34+
Name: Node,
35+
Metadata: map[string]interface{}{
36+
"launch": true,
37+
},
38+
},
39+
{
40+
Name: Yarn,
41+
Metadata: map[string]interface{}{
42+
"launch": true,
43+
},
44+
},
45+
{
46+
Name: NodeModules,
47+
Metadata: map[string]interface{}{
48+
"launch": true,
49+
},
50+
},
51+
}
52+
53+
shouldReload, err := checkLiveReloadEnabled()
54+
if err != nil {
55+
return packit.DetectResult{}, err
56+
}
57+
58+
if shouldReload {
59+
requirements = append(requirements, packit.BuildPlanRequirement{
60+
Name: "watchexec",
61+
Metadata: map[string]interface{}{
62+
"launch": true,
63+
},
64+
})
65+
}
66+
3167
return packit.DetectResult{
3268
Plan: packit.BuildPlan{
33-
Requires: []packit.BuildPlanRequirement{
34-
{
35-
Name: Node,
36-
Metadata: map[string]interface{}{
37-
"launch": true,
38-
},
39-
},
40-
{
41-
Name: Yarn,
42-
Metadata: map[string]interface{}{
43-
"launch": true,
44-
},
45-
},
46-
{
47-
Name: NodeModules,
48-
Metadata: map[string]interface{}{
49-
"launch": true,
50-
},
51-
},
52-
},
69+
Requires: requirements,
5370
},
5471
}, nil
5572
}
5673
}
74+
75+
func checkLiveReloadEnabled() (bool, error) {
76+
if reload, ok := os.LookupEnv("BP_LIVE_RELOAD_ENABLED"); ok {
77+
shouldEnableReload, err := strconv.ParseBool(reload)
78+
if err != nil {
79+
return false, fmt.Errorf("failed to parse BP_LIVE_RELOAD_ENABLED value %s: %w", reload, err)
80+
}
81+
return shouldEnableReload, nil
82+
}
83+
return false, nil
84+
}

detect_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,50 @@ func testDetect(t *testing.T, context spec.G, it spec.S) {
7373
}))
7474
Expect(projectPathParser.GetCall.Receives.Path).To(Equal(filepath.Join(workingDir)))
7575
})
76+
77+
context("and BP_LIVE_RELOAD_ENABLED=true in the build environment", func() {
78+
it.Before(func() {
79+
os.Setenv("BP_LIVE_RELOAD_ENABLED", "true")
80+
})
81+
82+
it.After(func() {
83+
os.Unsetenv("BP_LIVE_RELOAD_ENABLED")
84+
})
85+
86+
it("requires watchexec at launch time", func() {
87+
result, err := detect(packit.DetectContext{
88+
WorkingDir: workingDir,
89+
})
90+
Expect(err).NotTo(HaveOccurred())
91+
Expect(result.Plan.Requires).To(Equal([]packit.BuildPlanRequirement{
92+
{
93+
Name: "node",
94+
Metadata: map[string]interface{}{
95+
"launch": true,
96+
},
97+
},
98+
{
99+
Name: "yarn",
100+
Metadata: map[string]interface{}{
101+
"launch": true,
102+
},
103+
},
104+
{
105+
Name: "node_modules",
106+
Metadata: map[string]interface{}{
107+
"launch": true,
108+
},
109+
},
110+
{
111+
Name: "watchexec",
112+
Metadata: map[string]interface{}{
113+
"launch": true,
114+
},
115+
},
116+
},
117+
))
118+
})
119+
})
76120
})
77121

78122
context("when there is no yarn.lock", func() {
@@ -114,5 +158,23 @@ func testDetect(t *testing.T, context spec.G, it spec.S) {
114158
Expect(err).To(MatchError("couldn't find directory"))
115159
})
116160
})
161+
162+
context("when BP_LIVE_RELOAD_ENABLED is set to an invalid value", func() {
163+
it.Before(func() {
164+
Expect(ioutil.WriteFile(filepath.Join(workingDir, "custom", "yarn.lock"), nil, 0644)).To(Succeed())
165+
os.Setenv("BP_LIVE_RELOAD_ENABLED", "not-a-bool")
166+
})
167+
168+
it.After(func() {
169+
os.Unsetenv("BP_LIVE_RELOAD_ENABLED")
170+
})
171+
172+
it("returns an error", func() {
173+
_, err := detect(packit.DetectContext{
174+
WorkingDir: workingDir,
175+
})
176+
Expect(err).To(MatchError(ContainSubstring("failed to parse BP_LIVE_RELOAD_ENABLED value not-a-bool")))
177+
})
178+
})
117179
})
118180
}

init_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
func TestUnitGoBuild(t *testing.T) {
11-
suite := spec.New("yarn-start", spec.Report(report.Terminal{}))
11+
suite := spec.New("yarn-start", spec.Report(report.Terminal{}), spec.Sequential())
1212
suite("Build", testBuild)
1313
suite("Detect", testDetect)
1414
suite("ProjectPathParser", testProjectPathParser)

integration.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"node-engine": "github.com/paketo-buildpacks/node-engine",
33
"yarn": "github.com/paketo-buildpacks/yarn",
4-
"yarn-install": "github.com/paketo-buildpacks/yarn-install"
4+
"yarn-install": "github.com/paketo-buildpacks/yarn-install",
5+
"watchexec": "github.com/paketo-buildpacks/watchexec"
56
}

0 commit comments

Comments
 (0)