Skip to content

Commit c55235d

Browse files
authored
Merge pull request #831 from CubikNeRubik/custom-npm-script-support
[Sealights][SLDEV-22556] - support instrumentation of defined npm script
2 parents eed11bc + d39071a commit c55235d

File tree

3 files changed

+316
-20
lines changed

3 files changed

+316
-20
lines changed

src/nodejs/hooks/sealights.go

Lines changed: 113 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ type SealightsParameters struct {
6060
ProxyPassword string
6161
ProjectRoot string
6262
TestStage string
63+
NpmRunScript string
6364
}
6465

6566
type SealightsRunOptions struct {
@@ -155,8 +156,17 @@ func (sl *SealightsHook) SetApplicationStartInProcfile(stager *libbuildpack.Stag
155156
originalStartCommand := string(bytes)
156157
_, usePackageJson := sl.usePackageJson(originalStartCommand, stager)
157158
if usePackageJson {
159+
// Extract script name from command or use configured default
160+
scriptName, err := sl.ExtractNpmRunScriptName(originalStartCommand)
161+
if err != nil {
162+
sl.Log.Warning("Failed to extract script name from command '%s', using configured default: %s", originalStartCommand, err)
163+
scriptName = sl.parameters.NpmRunScript
164+
if scriptName == "" {
165+
scriptName = "start"
166+
}
167+
}
158168
// move to package json scenario
159-
return sl.SetApplicationStartInPackageJson(stager)
169+
return sl.SetApplicationStartInPackageJson(stager, scriptName)
160170
}
161171

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

195+
func (sl *SealightsHook) ExtractNpmRunScriptName(command string) (string, error) {
196+
// Remove leading "web:" prefix if present
197+
cleanCommand := strings.TrimSpace(command)
198+
if strings.HasPrefix(cleanCommand, "web:") {
199+
cleanCommand = strings.TrimSpace(cleanCommand[4:])
200+
}
201+
202+
// Handle commands with cd prefix (e.g., "cd app && npm run start")
203+
if strings.Contains(cleanCommand, "&&") {
204+
parts := strings.Split(cleanCommand, "&&")
205+
if len(parts) >= 2 {
206+
cleanCommand = strings.TrimSpace(parts[len(parts)-1])
207+
}
208+
}
209+
210+
// Extract script name from npm commands
211+
// Patterns to match:
212+
// - "npm start" -> "start"
213+
// - "npm run start-dev" -> "start-dev"
214+
// - "npm run dev" -> "dev"
215+
patterns := []string{
216+
`^npm\s+run\s+([a-zA-Z0-9\-_]+)`, // npm run <script>
217+
`^npm\s+([a-zA-Z0-9\-_]+)`, // npm <script>
218+
}
219+
220+
for _, pattern := range patterns {
221+
re, err := regexp.Compile(pattern)
222+
if err != nil {
223+
sl.Log.Warning("Failed to compile regex pattern %s: %s", pattern, err)
224+
continue
225+
}
226+
227+
matches := re.FindStringSubmatch(cleanCommand)
228+
if len(matches) >= 2 {
229+
scriptName := matches[1]
230+
sl.Log.Debug("Extracted npm script name: %s from command: %s", scriptName, command)
231+
return scriptName, nil
232+
}
233+
}
234+
235+
return "", fmt.Errorf("failed to extract npm script name from command: %s", command)
236+
}
237+
238+
func (sl *SealightsHook) ValidateNpmRunScript(packageJson map[string]interface{}, scriptName string) error {
239+
scripts, ok := packageJson["scripts"].(map[string]interface{})
240+
if !ok || scripts == nil {
241+
return fmt.Errorf("no scripts section found in package.json")
242+
}
243+
244+
if _, exists := scripts[scriptName]; !exists {
245+
return fmt.Errorf("script '%s' not found in package.json", scriptName)
246+
}
247+
248+
return nil
249+
}
250+
185251
func (sl *SealightsHook) usePackageJson(originalStartCommand string, stager *libbuildpack.Stager) (error, bool) {
186252

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

254-
func (sl *SealightsHook) SetApplicationStartInPackageJson(stager *libbuildpack.Stager) error {
320+
func (sl *SealightsHook) SetApplicationStartInPackageJson(stager *libbuildpack.Stager, targetScript string) error {
255321
packageJson, err := sl.ReadPackageJson(stager)
256322
if err != nil {
257323
return err
258324
}
259-
scripts, _ := packageJson["scripts"].(map[string]interface{})
260-
if scripts == nil {
261-
return fmt.Errorf("failed to read scripts from %s", PackageJsonFile)
325+
326+
// Validate that the target script exists
327+
err = sl.ValidateNpmRunScript(packageJson, targetScript)
328+
if err != nil {
329+
// Try fallback to "start" if configured script doesn't exist
330+
if targetScript != "start" {
331+
sl.Log.Warning("Script '%s' not found, falling back to 'start': %s", targetScript, err)
332+
fallbackErr := sl.ValidateNpmRunScript(packageJson, "start")
333+
if fallbackErr != nil {
334+
return fmt.Errorf("target script '%s' not found and fallback to 'start' failed: %s", targetScript, fallbackErr)
335+
}
336+
targetScript = "start"
337+
} else {
338+
return err
339+
}
262340
}
263-
originalStartScript, _ := scripts["start"].(string)
341+
342+
scripts := packageJson["scripts"].(map[string]interface{})
343+
originalStartScript, _ := scripts[targetScript].(string)
264344
if originalStartScript == "" {
265-
return fmt.Errorf("failed to read start from scripts in %s", PackageJsonFile)
345+
return fmt.Errorf("failed to read %s script from %s", targetScript, PackageJsonFile)
266346
}
267-
// we suppose that format is "start: node <application>"
347+
348+
// Update the command with Sealights injection
268349
var newCmd string
269350
newCmd, err = sl.updateStartCommand(originalStartScript)
270351
if err != nil {
271352
return err
272353
}
273-
packageJson["scripts"].(map[string]interface{})["start"] = newCmd
354+
355+
sl.Log.Debug("Injecting Sealights into '%s' script: %s -> %s", targetScript, originalStartScript, newCmd)
356+
scripts[targetScript] = newCmd
274357

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

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

304387
_, usePackageJson := sl.usePackageJson(originalStartCommand, stager)
305388
if usePackageJson {
389+
// Extract script name from command or use configured default
390+
scriptName, err := sl.ExtractNpmRunScriptName(originalStartCommand)
391+
if err != nil {
392+
sl.Log.Warning("Failed to extract script name from command '%s', using configured default: %s", originalStartCommand, err)
393+
scriptName = sl.parameters.NpmRunScript
394+
if scriptName == "" {
395+
scriptName = "start"
396+
}
397+
}
306398
// move to package json scenario
307-
return sl.SetApplicationStartInPackageJson(stager)
399+
return sl.SetApplicationStartInPackageJson(stager, scriptName)
308400
}
309401

310-
// we suppose that format is "start: node <application>"
402+
// we suppose that format is "node <application>"
311403
var newCmd string
312404
newCmd, err = sl.updateStartCommand(originalStartCommand)
313405
if err != nil {
@@ -437,8 +529,13 @@ func (sl *SealightsHook) injectSealights(stager *libbuildpack.Stager) error {
437529
sl.Log.Info("Integrating sealights into manifest.yml")
438530
return sl.SetApplicationStartInManifest(stager)
439531
} else {
440-
sl.Log.Info("Integrating sealights into package.json")
441-
return sl.SetApplicationStartInPackageJson(stager)
532+
sl.Log.Info("Integrating sealights into package.json")
533+
// Use configured script name or default to "start"
534+
scriptName := sl.parameters.NpmRunScript
535+
if scriptName == "" {
536+
scriptName = "start"
537+
}
538+
return sl.SetApplicationStartInPackageJson(stager, scriptName)
442539
}
443540
}
444541

@@ -487,6 +584,7 @@ func (sl *SealightsHook) parseVcapServices() {
487584
ProxyPassword: queryString("proxyPassword"),
488585
ProjectRoot: queryString("projectRoot"),
489586
TestStage: queryString("testStage"),
587+
NpmRunScript: queryString("npmRunScript"),
490588
}
491589

492590
// write warning in case token is not provided

0 commit comments

Comments
 (0)