Skip to content

Commit cf90fba

Browse files
authored
[planners] First pass on node js planner (#41)
## Summary Node JS planner initial pass. TODOs: [] Dockerfile changes are missing from this PR. (Trying to avoid unnecessary merge conflicts) [] We may need custom base images for start stage (pending #52) [] NodeJS examples will be in after the above feature goes in See #8 ## How was it tested? `devbox shell` `devbox build`
1 parent 225b13c commit cf90fba

File tree

13 files changed

+639
-31
lines changed

13 files changed

+639
-31
lines changed

examples/nodejs/index.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const express = require('express')
2+
const app = express()
3+
const port = 3000
4+
5+
app.get('/', (req, res) => {
6+
res.send('Hello World!')
7+
})
8+
9+
app.get('/ping', (req, res) => {
10+
res.send('Pong!')
11+
})
12+
13+
app.listen(port, () => {
14+
console.log(`Example app listening on port ${port}`)
15+
})

examples/nodejs/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "nodejs",
3+
"version": "1.0.0",
4+
"description": "Devbox NodeJS example",
5+
"main": "index.js",
6+
"dependencies": {
7+
"express": "^4.18.1"
8+
}
9+
}

examples/nodejs/yarn.lock

Lines changed: 405 additions & 0 deletions
Large diffs are not rendered by default.

generate.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ import (
2121
var tmplFS embed.FS
2222

2323
var shellFiles = []string{".gitignore", "shell.nix"}
24-
25-
// TODO: we should also generate a .dockerignore file
26-
var buildFiles = []string{".gitignore", "development.nix", "runtime.nix", "Dockerfile"}
24+
var buildFiles = []string{".gitignore", "development.nix", "runtime.nix", "Dockerfile", "Dockerfile.dockerignore"}
2725

2826
func generate(rootPath string, plan *planner.Plan, files []string) error {
2927
outPath := filepath.Join(rootPath, ".devbox/gen")

planner/go_planner.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ func (g *GoPlanner) GetPlan(srcDir string) *Plan {
4848
},
4949
StartStage: &Stage{
5050
Command: "./app",
51-
Image: "gcr.io/distroless/base:debug",
5251
},
5352
},
5453
}

planner/nodejs_planner.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2022 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package planner
5+
6+
import (
7+
"fmt"
8+
"path/filepath"
9+
10+
"go.jetpack.io/devbox/cuecfg"
11+
)
12+
13+
type NodeJSPlanner struct{}
14+
15+
// NodeJsPlanner implements interface Planner (compile-time check)
16+
var _ Planner = (*NodeJSPlanner)(nil)
17+
18+
func (n *NodeJSPlanner) Name() string {
19+
return "NodeJsPlanner"
20+
}
21+
22+
func (n *NodeJSPlanner) IsRelevant(srcDir string) bool {
23+
packageJSONPath := filepath.Join(srcDir, "package.json")
24+
return fileExists(packageJSONPath)
25+
}
26+
27+
func (n *NodeJSPlanner) GetPlan(srcDir string) *Plan {
28+
packages := []string{n.nodePackage(srcDir)}
29+
pkgManager := "npm"
30+
inputFiles := []string{
31+
filepath.Join(srcDir, "package.json"),
32+
}
33+
34+
npmPkgLockPath := filepath.Join(srcDir, "package-lock.json")
35+
if fileExists(npmPkgLockPath) {
36+
inputFiles = append(inputFiles, npmPkgLockPath)
37+
}
38+
39+
yarnPkgLockPath := filepath.Join(srcDir, "yarn.lock")
40+
if fileExists(yarnPkgLockPath) {
41+
pkgManager = "yarn"
42+
packages = append(packages, "yarn")
43+
inputFiles = append(inputFiles, yarnPkgLockPath)
44+
}
45+
46+
return &Plan{
47+
DevPackages: packages,
48+
// TODO: Optimize runtime packages to remove npm or yarn if startStage command use Node directly.
49+
RuntimePackages: packages,
50+
51+
SharedPlan: SharedPlan{
52+
InstallStage: &Stage{
53+
InputFiles: inputFiles,
54+
Command: fmt.Sprintf("%s install", pkgManager),
55+
},
56+
57+
BuildStage: &Stage{
58+
// Copy the rest of the directory over, since at install stage we only copied package.json and its lock file.
59+
InputFiles: []string{"."},
60+
// Command: "" (command should be set by users. Some apps don't require a build command.)
61+
},
62+
63+
StartStage: &Stage{
64+
// Start command could be `Node server.js`, `npm serve`, `yarn start`, or anything really.
65+
// For now we use `node index.js` as the default.
66+
Command: "node index.js",
67+
},
68+
},
69+
}
70+
}
71+
72+
type nodeProject struct {
73+
Engines struct {
74+
Node string `json:"node,omitempty"`
75+
} `json:"engines,omitempty"`
76+
}
77+
78+
func (n *NodeJSPlanner) nodePackage(srcDir string) string {
79+
v := n.nodeVersion(srcDir)
80+
if v != nil {
81+
switch v.major() {
82+
case "10":
83+
return "nodejs-10_x"
84+
case "12":
85+
return "nodejs-12_x"
86+
case "16":
87+
return "nodejs-16_x"
88+
case "18":
89+
return "nodejs-18_x"
90+
}
91+
}
92+
93+
return "nodejs"
94+
}
95+
96+
func (n *NodeJSPlanner) nodeVersion(srcDir string) *version {
97+
p := n.nodeProject(srcDir)
98+
if p != nil {
99+
if v, err := newVersion(p.Engines.Node); err == nil {
100+
return v
101+
}
102+
}
103+
104+
return nil
105+
}
106+
107+
func (n *NodeJSPlanner) nodeProject(srcDir string) *nodeProject {
108+
packageJSONPath := filepath.Join(srcDir, "package.json")
109+
p := &nodeProject{}
110+
_ = cuecfg.ReadFile(packageJSONPath, p)
111+
112+
return p
113+
}

planner/plan.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ type SharedPlan struct {
4848

4949
type Stage struct {
5050
Command string `cue:"string" json:"command"`
51-
Image string `json:"-"`
51+
// InputFiles is internal for planners only.
52+
InputFiles []string `cue:"[...string]" json:"input_files,omitempty"`
5253
}
5354

5455
func (p *Plan) String() string {
@@ -100,6 +101,11 @@ func MergePlans(plans ...*Plan) *Plan {
100101
plan := &Plan{
101102
DevPackages: []string{},
102103
RuntimePackages: []string{},
104+
SharedPlan: SharedPlan{
105+
InstallStage: &Stage{},
106+
BuildStage: &Stage{},
107+
StartStage: &Stage{},
108+
},
103109
}
104110
for _, p := range plans {
105111
err := mergo.Merge(plan, p, mergo.WithAppendSlice)
@@ -111,6 +117,15 @@ func MergePlans(plans ...*Plan) *Plan {
111117
plan.DevPackages = pkgslice.Unique(plan.DevPackages)
112118
plan.RuntimePackages = pkgslice.Unique(plan.RuntimePackages)
113119

120+
// Set default files for install stage to copy.
121+
if plan.SharedPlan.InstallStage.InputFiles == nil {
122+
plan.SharedPlan.InstallStage.InputFiles = []string{"."}
123+
}
124+
// Set default files for install stage to copy over from build step.
125+
if plan.SharedPlan.StartStage.InputFiles == nil {
126+
plan.SharedPlan.StartStage.InputFiles = []string{"."}
127+
}
128+
114129
return plan
115130
}
116131

planner/plan_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@ func TestMergePlans(t *testing.T) {
2222
expected := &Plan{
2323
DevPackages: []string{"foo", "bar", "baz"},
2424
RuntimePackages: []string{"a", "b", "c"},
25+
SharedPlan: SharedPlan{
26+
InstallStage: &Stage{
27+
Command: "",
28+
InputFiles: []string{"."},
29+
},
30+
BuildStage: &Stage{
31+
Command: "",
32+
},
33+
StartStage: &Stage{
34+
Command: "",
35+
InputFiles: []string{"."},
36+
},
37+
},
2538
}
2639
actual := MergePlans(plan1, plan2)
2740
assert.Equal(t, expected, actual)
@@ -45,9 +58,51 @@ func TestMergePlans(t *testing.T) {
4558
DevPackages: []string{},
4659
RuntimePackages: []string{},
4760
SharedPlan: SharedPlan{
61+
InstallStage: &Stage{
62+
Command: "",
63+
InputFiles: []string{"."},
64+
},
4865
BuildStage: &Stage{
4966
Command: "plan1",
5067
},
68+
StartStage: &Stage{
69+
Command: "",
70+
InputFiles: []string{"."},
71+
},
72+
},
73+
}
74+
actual = MergePlans(plan1, plan2)
75+
assert.Equal(t, expected, actual)
76+
77+
// InputFiles can be overwritten:
78+
plan1 = &Plan{
79+
SharedPlan: SharedPlan{
80+
InstallStage: &Stage{
81+
InputFiles: []string{"package.json"},
82+
},
83+
StartStage: &Stage{
84+
InputFiles: []string{"input"},
85+
},
86+
},
87+
}
88+
plan2 = &Plan{
89+
SharedPlan: SharedPlan{},
90+
}
91+
expected = &Plan{
92+
DevPackages: []string{},
93+
RuntimePackages: []string{},
94+
SharedPlan: SharedPlan{
95+
InstallStage: &Stage{
96+
Command: "",
97+
InputFiles: []string{"package.json"},
98+
},
99+
BuildStage: &Stage{
100+
Command: "",
101+
},
102+
StartStage: &Stage{
103+
Command: "",
104+
InputFiles: []string{"input"},
105+
},
51106
},
52107
}
53108
actual = MergePlans(plan1, plan2)

planner/planner.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ var PLANNERS = []Planner{
1515
&GoPlanner{},
1616
&PHPPlanner{},
1717
&PythonPoetryPlanner{},
18+
&NodeJSPlanner{},
1819
}
1920

2021
func GetPlan(srcDir string) *Plan {

planner/python_poetry_planner.go

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ func (g *PythonPoetryPlanner) IsRelevant(srcDir string) bool {
3030

3131
func (g *PythonPoetryPlanner) GetPlan(srcDir string) *Plan {
3232
version := g.PythonVersion(srcDir)
33+
pythonPkg := fmt.Sprintf("python%s", version.majorMinorConcatenated())
3334
plan := &Plan{
3435
DevPackages: []string{
35-
fmt.Sprintf("python%s", version.majorMinorConcatenated()),
36+
pythonPkg,
3637
"poetry",
3738
},
39+
RuntimePackages: []string{pythonPkg},
3840
}
3941
if buildable, err := g.isBuildable(srcDir); !buildable {
4042
return plan.WithError(err)
@@ -56,7 +58,6 @@ func (g *PythonPoetryPlanner) GetPlan(srcDir string) *Plan {
5658
}
5759
plan.StartStage = &Stage{
5860
Command: "PEX_ROOT=/tmp/.pex python ./app.pex",
59-
Image: getPythonImage(version),
6061
}
6162
return plan
6263
}
@@ -115,16 +116,6 @@ func (g *PythonPoetryPlanner) PyProject(srcDir string) *pyProject {
115116
return &p
116117
}
117118

118-
func getPythonImage(version *version) string {
119-
if version.exact() == "3" {
120-
return "al3xos/python-distroless:3.10-debian11-debug"
121-
}
122-
if version.majorMinor() == "3.10" || version.majorMinor() == "3.9" {
123-
return fmt.Sprintf("al3xos/python-distroless:%s-debian11-debug", version.majorMinor())
124-
}
125-
return fmt.Sprintf("python:%s-slim", version.exact())
126-
}
127-
128119
func (g *PythonPoetryPlanner) isBuildable(srcDir string) (bool, error) {
129120
project := g.PyProject(srcDir)
130121
if project == nil {

0 commit comments

Comments
 (0)