basic node provider

This commit is contained in:
Jake Runzer
2025-01-14 19:38:48 -05:00
parent 73f03de56f
commit e838d33dfd
15 changed files with 443 additions and 17 deletions
+2 -2
View File
@@ -42,12 +42,12 @@ var BuildCommand = &cli.Command{
return cli.Exit(err, 1)
}
plan, err := core.GenerateBuildPlan(userApp, env, &core.GenerateBuildPlanOptions{})
buildResult, err := core.GenerateBuildPlan(userApp, env, &core.GenerateBuildPlanOptions{})
if err != nil {
return cli.Exit(err, 1)
}
serializedPlan, err := json.MarshalIndent(plan, "", " ")
serializedPlan, err := json.MarshalIndent(buildResult, "", " ")
if err != nil {
return cli.Exit(err, 1)
}
+2 -1
View File
@@ -32,7 +32,8 @@ func TestApp(t *testing.T) {
require.Contains(t, content, "node-bun")
var packageJSON PackageJSON
app.ReadJSON("package.json", &packageJSON)
err = app.ReadJSON("package.json", &packageJSON)
require.NoError(t, err)
require.Equal(t, packageJSON.Name, "node-bun")
files, err := app.FindFiles("*.ts")
+24
View File
@@ -1,8 +1,10 @@
package app
import (
"fmt"
"os"
"regexp"
"strings"
)
type Environment struct {
@@ -54,3 +56,25 @@ func (e *Environment) GetVariable(name string) string {
func (e *Environment) SetVariable(name, value string) {
e.Variables[name] = value
}
// ConfigVariable returns the RAILPACK_ prefixed version of a variable name
func (e *Environment) ConfigVariable(name string) string {
return fmt.Sprintf("RAILPACK_%s", name)
}
// GetConfigVariable returns the value of a RAILPACK_ prefixed variable with newlines removed
func (e *Environment) GetConfigVariable(name string) string {
configVar := e.ConfigVariable(name)
if val, exists := e.Variables[configVar]; exists {
return strings.ReplaceAll(val, "\n", "")
}
return ""
}
// IsConfigVariableTruthy checks if a RAILPACK_ prefixed variable is set to "1" or "true"
func (e *Environment) IsConfigVariableTruthy(name string) bool {
if val := e.GetConfigVariable(name); val != "" {
return val == "1" || val == "true"
}
return false
}
+36 -4
View File
@@ -8,12 +8,17 @@ import (
"github.com/railwayapp/railpack-go/core/generate"
"github.com/railwayapp/railpack-go/core/plan"
"github.com/railwayapp/railpack-go/core/providers"
"github.com/railwayapp/railpack-go/core/resolver"
)
type GenerateBuildPlanOptions struct {
type GenerateBuildPlanOptions struct{}
type BuildResult struct {
Plan *plan.BuildPlan `json:"plan"`
ResolvedPackages map[string]*resolver.ResolvedPackage `json:"resolved_packages"`
}
func GenerateBuildPlan(app *app.App, env *app.Environment, options *GenerateBuildPlanOptions) (*plan.BuildPlan, error) {
func GenerateBuildPlan(app *app.App, env *app.Environment, options *GenerateBuildPlanOptions) (*BuildResult, error) {
ctx, err := generate.NewGenerateContext(app, env)
if err != nil {
return nil, err
@@ -26,12 +31,39 @@ func GenerateBuildPlan(app *app.App, env *app.Environment, options *GenerateBuil
}
if matched {
log.Debugf("Provider %s matched", provider)
log.Debugf("Provider `%s` matched", provider.Name())
break
}
}
return nil, nil
resolvedPackages, err := ctx.Resolver.ResolvePackages()
if err != nil {
return nil, fmt.Errorf("failed to resolve packages: %w", err)
}
buildPlan := plan.NewBuildPlan()
buildPlan.Variables = ctx.Variables
for _, step := range ctx.Steps {
buildPlan.AddStep(*step)
}
for _, aptPackage := range ctx.AptPackages {
buildPlan.Packages.AddAptPackage(aptPackage)
}
for _, resolvedPackage := range resolvedPackages {
if resolvedPackage.ResolvedVersion != nil {
buildPlan.Packages.AddMisePackage(resolvedPackage.Name, *resolvedPackage.ResolvedVersion)
}
}
buildResult := &BuildResult{
Plan: buildPlan,
ResolvedPackages: resolvedPackages,
}
return buildResult, nil
}
func runProvider(provider providers.Provider, ctx *generate.GenerateContext) (bool, error) {
+4 -4
View File
@@ -8,8 +8,8 @@ import (
)
type GenerateContext struct {
app *app.App
env *app.Environment
App *app.App
Env *app.Environment
Resolver *resolver.Resolver
Variables map[string]string
AptPackages []string
@@ -23,8 +23,8 @@ func NewGenerateContext(app *app.App, env *app.Environment) (*GenerateContext, e
}
return &GenerateContext{
app: app,
env: env,
App: app,
Env: env,
Resolver: resolver,
Variables: map[string]string{},
AptPackages: []string{},
+51
View File
@@ -0,0 +1,51 @@
package plan
// Command represents a command that can be executed
type Command interface {
commandType() string
}
// ExecCommand represents a command to be executed
type ExecCommand struct {
Cmd string `json:"cmd"`
}
// GlobalPathCommand represents a global path addition
type GlobalPathCommand struct {
GlobalPath string `json:"globalPath"`
}
// VariableCommand represents a shell variable setting
type VariableCommand struct {
Name string `json:"name"`
Value string `json:"value"`
}
// CopyCommand represents a file copy operation
type CopyCommand struct {
Src string `json:"src"`
Dst string `json:"dst"`
}
// Implementation of Command interface for each type
func (e ExecCommand) commandType() string { return "exec" }
func (g GlobalPathCommand) commandType() string { return "globalPath" }
func (v VariableCommand) commandType() string { return "variable" }
func (c CopyCommand) commandType() string { return "copy" }
// Constructor functions
func NewExecCommand(cmd string) Command {
return ExecCommand{Cmd: cmd}
}
func NewGlobalPathCommand(path string) Command {
return GlobalPathCommand{GlobalPath: path}
}
func NewVariableCommand(name, value string) Command {
return VariableCommand{Name: name, Value: value}
}
func NewCopyCommand(src, dst string) Command {
return CopyCommand{Src: src, Dst: dst}
}
+56
View File
@@ -0,0 +1,56 @@
package plan
import (
"bytes"
"github.com/BurntSushi/toml"
)
type PlanPackages struct {
Apt []string `json:"apt,omitempty"`
Mise map[string]string `json:"mise,omitempty"`
}
func NewPlanPackages() *PlanPackages {
return &PlanPackages{
Apt: []string{},
Mise: map[string]string{},
}
}
func (p *PlanPackages) AddAptPackage(pkg string) {
p.Apt = append(p.Apt, pkg)
}
func (p *PlanPackages) AddMisePackage(pkg string, version string) {
p.Mise[pkg] = version
}
// MisePackage represents a single mise package configuration
type MisePackage struct {
Version string `toml:"version"`
}
// MiseConfig represents the overall mise configuration
type MiseConfig struct {
Tools map[string]MisePackage `toml:"tools"`
}
func (p *PlanPackages) GenerateMiseToml() (string, error) {
config := MiseConfig{
Tools: make(map[string]MisePackage),
}
for name, version := range p.Mise {
config.Tools[name] = MisePackage{
Version: version,
}
}
buf := bytes.NewBuffer(nil)
if err := toml.NewEncoder(buf).Encode(config); err != nil {
return "", err
}
return buf.String(), nil
}
+2
View File
@@ -5,12 +5,14 @@ import "encoding/json"
type BuildPlan struct {
Variables map[string]string `json:"variables,omitempty"`
Steps []Step `json:"steps,omitempty"`
Packages *PlanPackages `json:"packages,omitempty"`
}
func NewBuildPlan() *BuildPlan {
return &BuildPlan{
Variables: map[string]string{},
Steps: []Step{},
Packages: NewPlanPackages(),
}
}
+19 -3
View File
@@ -3,8 +3,24 @@ package plan
type Step struct {
Name string `json:"name"`
DependsOn []string `json:"depends_on,omitempty"`
Commands []string `json:"commands,omitempty"`
DependsOn []string `json:"depends_on,omitempty"`
Commands []Command `json:"commands,omitempty"`
}
func NewStep(name string) *Step {
return &Step{
Name: name,
DependsOn: make([]string, 0),
Commands: make([]Command, 0),
}
}
func (s *Step) DependOn(name string) {
s.DependsOn = append(s.DependsOn, name)
}
func (s *Step) AddCommands(commands []Command) {
s.Commands = append(s.Commands, commands...)
}
func MergeSteps(steps ...*Step) *Step {
@@ -15,7 +31,7 @@ func MergeSteps(steps ...*Step) *Step {
result := &Step{
Name: steps[0].Name,
DependsOn: make([]string, 0),
Commands: make([]string, 0),
Commands: make([]Command, 0),
}
for _, step := range steps {
+180 -3
View File
@@ -1,13 +1,190 @@
package node
import "github.com/railwayapp/railpack-go/core/generate"
import (
"fmt"
"strings"
"github.com/railwayapp/railpack-go/core/app"
"github.com/railwayapp/railpack-go/core/generate"
"github.com/railwayapp/railpack-go/core/plan"
)
type PackageJson struct {
Scripts map[string]string `json:"scripts"`
PackageManager *string `json:"packageManager"`
Engines map[string]string `json:"engines"`
}
type PackageManager string
const (
DEFAULT_NODE_VERSION = "22"
DEFAULT_BUN_VERSION = "latest"
)
type NodeProvider struct{}
const DEFAULT_NODE_VERSION = "22"
func (p *NodeProvider) Name() string {
return "node"
}
func (p *NodeProvider) Plan(ctx *generate.GenerateContext) (bool, error) {
ctx.Resolver.Default("node", DEFAULT_NODE_VERSION)
packageJson, err := p.getPackageJson(ctx.App)
if err != nil {
return false, err
}
if packageJson == nil {
return false, nil
}
if err := p.packages(ctx, packageJson); err != nil {
return false, err
}
if err := p.install(ctx, packageJson); err != nil {
return false, err
}
// if err := p.build(ctx, packageJson); err != nil {
// return false, err
// }
return true, nil
}
func (p *NodeProvider) install(ctx *generate.GenerateContext, packageJson *PackageJson) error {
corepack := p.usesCorepack(packageJson)
if corepack {
setup := plan.NewStep("setup")
setup.AddCommands([]plan.Command{
plan.NewCopyCommand("package.json", "."),
plan.NewExecCommand("npm install -g corepack"),
plan.NewExecCommand("corepack enable"),
plan.NewExecCommand("corepack prepare --activate"),
})
ctx.AddStep(setup)
}
pkgManager := p.getPackageManager(ctx.App)
install := plan.NewStep("install")
install.AddCommands([]plan.Command{
plan.NewCopyCommand(".", "."),
plan.NewExecCommand(pkgManager.InstallDeps()),
})
if corepack {
install.DependOn("setup")
}
ctx.AddStep(install)
return nil
}
func (p *NodeProvider) packages(ctx *generate.GenerateContext, packageJson *PackageJson) error {
packageManager := p.getPackageManager(ctx.App)
// Node
node := ctx.Resolver.Default("node", DEFAULT_NODE_VERSION)
envVersion := ctx.Env.GetConfigVariable("NODE_VERSION")
if envVersion != "" {
ctx.Resolver.Version(node, envVersion, "RAILPACK_NODE_VERSION")
}
if packageJson.Engines != nil && packageJson.Engines["node"] != "" {
ctx.Resolver.Version(node, packageJson.Engines["node"], "package.json > engines > node")
}
if packageManager == PackageManagerBun {
bun := ctx.Resolver.Default("bun", DEFAULT_BUN_VERSION)
envVersion := ctx.Env.GetConfigVariable("BUN_VERSION")
if envVersion != "" {
ctx.Resolver.Version(bun, envVersion, "RAILPACK_BUN_VERSION")
}
}
return p.managerPackages(ctx, packageManager)
}
func (p *NodeProvider) managerPackages(ctx *generate.GenerateContext, packageManager PackageManager) error {
// NPM
if packageManager == PackageManagerNpm {
npm := ctx.Resolver.Default("npm", "latest")
lockfile, err := ctx.App.ReadFile("package-lock.json")
if err != nil {
return fmt.Errorf("error reading package-lock.json: %w", err)
}
if strings.Contains(lockfile, "\"lockfileVersion\": 1") {
ctx.Resolver.Version(npm, "6", "package-lock.json")
} else if strings.Contains(lockfile, "\"lockfileVersion\": 2") {
ctx.Resolver.Version(npm, "8", "package-lock.json")
}
}
// Pnpm
if packageManager == PackageManagerPnpm {
pnpm := ctx.Resolver.Default("pnpm", "latest")
lockfile, err := ctx.App.ReadFile("pnpm-lock.yaml")
if err == nil {
if strings.HasPrefix(lockfile, "lockfileVersion: 5.3") {
ctx.Resolver.Version(pnpm, "6", "pnpm-lock.yaml")
} else if strings.HasPrefix(lockfile, "lockfileVersion: 5.4") {
ctx.Resolver.Version(pnpm, "7", "pnpm-lock.yaml")
} else if strings.HasPrefix(lockfile, "lockfileVersion: '6.0'") {
ctx.Resolver.Version(pnpm, "8", "pnpm-lock.yaml")
}
}
}
// Yarn
if packageManager == PackageManagerYarn1 {
ctx.Resolver.Default("yarn", "1")
ctx.AddAptPackage("tar")
ctx.AddAptPackage("gpg")
} else if packageManager == PackageManagerYarn2 {
ctx.Resolver.Default("yarn", "2")
}
return nil
}
func (p *NodeProvider) usesCorepack(packageJson *PackageJson) bool {
return packageJson.PackageManager != nil
}
func (p *NodeProvider) getPackageManager(app *app.App) PackageManager {
packageManager := PackageManagerNpm
if app.HasMatch("pnpm-lock.yaml") {
packageManager = PackageManagerPnpm
} else if app.HasMatch("bun.lockb") || app.HasMatch("bun.lock") {
packageManager = PackageManagerBun
} else if app.HasMatch(".yarnrc.yml") || app.HasMatch(".yarnrc.yaml") {
packageManager = PackageManagerYarn2
} else if app.HasMatch("yarn.lock") {
packageManager = PackageManagerYarn1
}
return packageManager
}
func (p *NodeProvider) getPackageJson(app *app.App) (*PackageJson, error) {
if !app.HasMatch("package.json") {
return nil, nil
}
var packageJson PackageJson
err := app.ReadJSON("package.json", &packageJson)
if err != nil {
return nil, fmt.Errorf("error reading package.json: %w", err)
}
return &packageJson, nil
}
+47
View File
@@ -0,0 +1,47 @@
package node
import "fmt"
const (
PackageManagerNpm PackageManager = "npm"
PackageManagerPnpm PackageManager = "pnpm"
PackageManagerBun PackageManager = "bun"
PackageManagerYarn1 PackageManager = "yarn1"
PackageManagerYarn2 PackageManager = "yarn2"
)
func (p PackageManager) Name() string {
switch p {
case PackageManagerNpm:
return "npm"
case PackageManagerPnpm:
return "pnpm"
case PackageManagerBun:
return "bun"
case PackageManagerYarn1, PackageManagerYarn2:
return "yarn"
default:
return ""
}
}
func (p PackageManager) InstallDeps() string {
switch p {
case PackageManagerNpm:
return "npm ci"
case PackageManagerPnpm:
return "pnpm install --frozen-lockfile"
case PackageManagerBun:
return "bun i --no-save"
case PackageManagerYarn1:
return "yarn install --frozen-lockfile"
case PackageManagerYarn2:
return "yarn install --check-cache"
default:
return ""
}
}
func (p PackageManager) RunCmd(cmd string) string {
return fmt.Sprintf("%s run %s", p.Name(), cmd)
}
+1
View File
@@ -6,6 +6,7 @@ import (
)
type Provider interface {
Name() string
Plan(ctx *generate.GenerateContext) (bool, error)
}
+2
View File
@@ -3,8 +3,10 @@ module github.com/railwayapp/railpack-go
go 1.23.3
require (
github.com/BurntSushi/toml v1.4.0
github.com/bmatcuk/doublestar/v4 v4.8.0
github.com/charmbracelet/log v0.4.0
github.com/simonnilsson/ask v0.4.0
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v3 v3.0.0-beta1
gopkg.in/yaml.v2 v2.4.0
+4
View File
@@ -1,3 +1,5 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ=
@@ -27,6 +29,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/simonnilsson/ask v0.4.0 h1:m+IaCOwgvXI1w9+g4PiCmxwBD/nEjd1ycc+oneQLMLU=
github.com/simonnilsson/ask v0.4.0/go.mod h1:sUb3WVJKlF20TzemrTDTB5b1e2mxYYqluNe75OPa7Tg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg=
+13
View File
@@ -7,5 +7,18 @@ run = "rm -rf bin"
[tasks.test]
run = "go test ./..."
[tasks.check]
run = """
go vet ./...
go fmt ./...
golangci-lint run
"""
[tasks.tidy]
run = "go mod tidy"
[tools.go]
version = "1.23.4"
[tools]
golangci-lint = "1.63.4"