Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 113 additions & 15 deletions src/nodejs/hooks/sealights.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type SealightsParameters struct {
ProxyPassword string
ProjectRoot string
TestStage string
NpmRunScript string
}

type SealightsRunOptions struct {
Expand Down Expand Up @@ -155,8 +156,17 @@ func (sl *SealightsHook) SetApplicationStartInProcfile(stager *libbuildpack.Stag
originalStartCommand := string(bytes)
_, usePackageJson := sl.usePackageJson(originalStartCommand, stager)
if usePackageJson {
// Extract script name from command or use configured default
scriptName, err := sl.ExtractNpmRunScriptName(originalStartCommand)
if err != nil {
sl.Log.Warning("Failed to extract script name from command '%s', using configured default: %s", originalStartCommand, err)
scriptName = sl.parameters.NpmRunScript
if scriptName == "" {
scriptName = "start"
}
}
// move to package json scenario
return sl.SetApplicationStartInPackageJson(stager)
return sl.SetApplicationStartInPackageJson(stager, scriptName)
}

// we suppose that format is "web: node <application>"
Expand All @@ -182,6 +192,62 @@ func (sl *SealightsHook) SetApplicationStartInProcfile(stager *libbuildpack.Stag
return nil
}

func (sl *SealightsHook) ExtractNpmRunScriptName(command string) (string, error) {
// Remove leading "web:" prefix if present
cleanCommand := strings.TrimSpace(command)
if strings.HasPrefix(cleanCommand, "web:") {
cleanCommand = strings.TrimSpace(cleanCommand[4:])
}

// Handle commands with cd prefix (e.g., "cd app && npm run start")
if strings.Contains(cleanCommand, "&&") {
parts := strings.Split(cleanCommand, "&&")
if len(parts) >= 2 {
cleanCommand = strings.TrimSpace(parts[len(parts)-1])
}
}

// Extract script name from npm commands
// Patterns to match:
// - "npm start" -> "start"
// - "npm run start-dev" -> "start-dev"
// - "npm run dev" -> "dev"
patterns := []string{
`^npm\s+run\s+([a-zA-Z0-9\-_]+)`, // npm run <script>
`^npm\s+([a-zA-Z0-9\-_]+)`, // npm <script>
}

for _, pattern := range patterns {
re, err := regexp.Compile(pattern)
if err != nil {
sl.Log.Warning("Failed to compile regex pattern %s: %s", pattern, err)
continue
}

matches := re.FindStringSubmatch(cleanCommand)
if len(matches) >= 2 {
scriptName := matches[1]
sl.Log.Debug("Extracted npm script name: %s from command: %s", scriptName, command)
return scriptName, nil
}
}

return "", fmt.Errorf("failed to extract npm script name from command: %s", command)
}

func (sl *SealightsHook) ValidateNpmRunScript(packageJson map[string]interface{}, scriptName string) error {
scripts, ok := packageJson["scripts"].(map[string]interface{})
if !ok || scripts == nil {
return fmt.Errorf("no scripts section found in package.json")
}

if _, exists := scripts[scriptName]; !exists {
return fmt.Errorf("script '%s' not found in package.json", scriptName)
}

return nil
}

func (sl *SealightsHook) usePackageJson(originalStartCommand string, stager *libbuildpack.Stager) (error, bool) {

isNpmCommand, err := regexp.MatchString(`(^(web:\s)?cd[^&]*\s&&\snpm)|(^(web:\s)?npm)`, originalStartCommand)
Expand Down Expand Up @@ -251,26 +317,43 @@ func (sl *SealightsHook) getSealightsOptions(app string) *SealightsRunOptions {
return o
}

func (sl *SealightsHook) SetApplicationStartInPackageJson(stager *libbuildpack.Stager) error {
func (sl *SealightsHook) SetApplicationStartInPackageJson(stager *libbuildpack.Stager, targetScript string) error {
packageJson, err := sl.ReadPackageJson(stager)
if err != nil {
return err
}
scripts, _ := packageJson["scripts"].(map[string]interface{})
if scripts == nil {
return fmt.Errorf("failed to read scripts from %s", PackageJsonFile)

// Validate that the target script exists
err = sl.ValidateNpmRunScript(packageJson, targetScript)
if err != nil {
// Try fallback to "start" if configured script doesn't exist
if targetScript != "start" {
sl.Log.Warning("Script '%s' not found, falling back to 'start': %s", targetScript, err)
fallbackErr := sl.ValidateNpmRunScript(packageJson, "start")
if fallbackErr != nil {
return fmt.Errorf("target script '%s' not found and fallback to 'start' failed: %s", targetScript, fallbackErr)
}
targetScript = "start"
} else {
return err
}
}
originalStartScript, _ := scripts["start"].(string)

scripts := packageJson["scripts"].(map[string]interface{})
originalStartScript, _ := scripts[targetScript].(string)
if originalStartScript == "" {
return fmt.Errorf("failed to read start from scripts in %s", PackageJsonFile)
return fmt.Errorf("failed to read %s script from %s", targetScript, PackageJsonFile)
}
// we suppose that format is "start: node <application>"

// Update the command with Sealights injection
var newCmd string
newCmd, err = sl.updateStartCommand(originalStartScript)
if err != nil {
return err
}
packageJson["scripts"].(map[string]interface{})["start"] = newCmd

sl.Log.Debug("Injecting Sealights into '%s' script: %s -> %s", targetScript, originalStartScript, newCmd)
scripts[targetScript] = newCmd

err = libbuildpack.NewJSON().Write(filepath.Join(stager.BuildDir(), PackageJsonFile), packageJson)
if err != nil {
Expand All @@ -284,9 +367,9 @@ func (sl *SealightsHook) SetApplicationStartInPackageJson(stager *libbuildpack.S
func (sl *SealightsHook) ReadPackageJson(stager *libbuildpack.Stager) (map[string]interface{}, error) {
p := map[string]interface{}{}

if err := libbuildpack.NewJSON().Load(filepath.Join(stager.BuildDir(), "package.json"), &p); err != nil {
if err := libbuildpack.NewJSON().Load(filepath.Join(stager.BuildDir(), PackageJsonFile), &p); err != nil {
if err != nil {
sl.Log.Error("failed to read %s error: %s", Procfile, err.Error())
sl.Log.Error("failed to read %s error: %s", PackageJsonFile, err.Error())
return nil, err
}
}
Expand All @@ -303,11 +386,20 @@ func (sl *SealightsHook) SetApplicationStartInManifest(stager *libbuildpack.Stag

_, usePackageJson := sl.usePackageJson(originalStartCommand, stager)
if usePackageJson {
// Extract script name from command or use configured default
scriptName, err := sl.ExtractNpmRunScriptName(originalStartCommand)
if err != nil {
sl.Log.Warning("Failed to extract script name from command '%s', using configured default: %s", originalStartCommand, err)
scriptName = sl.parameters.NpmRunScript
if scriptName == "" {
scriptName = "start"
}
}
// move to package json scenario
return sl.SetApplicationStartInPackageJson(stager)
return sl.SetApplicationStartInPackageJson(stager, scriptName)
}

// we suppose that format is "start: node <application>"
// we suppose that format is "node <application>"
var newCmd string
newCmd, err = sl.updateStartCommand(originalStartCommand)
if err != nil {
Expand Down Expand Up @@ -437,8 +529,13 @@ func (sl *SealightsHook) injectSealights(stager *libbuildpack.Stager) error {
sl.Log.Info("Integrating sealights into manifest.yml")
return sl.SetApplicationStartInManifest(stager)
} else {
sl.Log.Info("Integrating sealights into package.json")
return sl.SetApplicationStartInPackageJson(stager)
sl.Log.Info("Integrating sealights into package.json")
// Use configured script name or default to "start"
scriptName := sl.parameters.NpmRunScript
if scriptName == "" {
scriptName = "start"
}
return sl.SetApplicationStartInPackageJson(stager, scriptName)
}
}

Expand Down Expand Up @@ -487,6 +584,7 @@ func (sl *SealightsHook) parseVcapServices() {
ProxyPassword: queryString("proxyPassword"),
ProjectRoot: queryString("projectRoot"),
TestStage: queryString("testStage"),
NpmRunScript: queryString("npmRunScript"),
}

// write warning in case token is not provided
Expand Down
Loading
Loading