@@ -15,6 +15,7 @@ import (
1515 "github.com/sst/sst/v3/pkg/js"
1616 "github.com/sst/sst/v3/pkg/process"
1717 "github.com/sst/sst/v3/pkg/runtime"
18+ "gopkg.in/yaml.v3"
1819)
1920
2021var forceExternal = []string {
@@ -273,7 +274,11 @@ func (r *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim
273274 }
274275 dependencies := map [string ]string {}
275276 for _ , pkg := range installPackages {
276- dependencies [pkg ] = resolveInstallVersion (pkg , properties .Install , parsed )
277+ version , err := resolveInstallVersion (pkg , properties .Install , filepath .Dir (src ), parsed )
278+ if err != nil {
279+ return nil , err
280+ }
281+ dependencies [pkg ] = version
277282 }
278283 outPkg := filepath .Join (input .Out (), "package.json" )
279284 outFile , err := os .Create (outPkg )
@@ -320,6 +325,22 @@ func (r *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim
320325 }, nil
321326}
322327
328+ type catalogSource struct {
329+ Catalog map [string ]string `json:"catalog" yaml:"catalog"`
330+ Catalogs map [string ]map [string ]string `json:"catalogs" yaml:"catalogs"`
331+ }
332+
333+ type bunPackageJSON struct {
334+ Catalog map [string ]string `json:"catalog"`
335+ Catalogs map [string ]map [string ]string `json:"catalogs"`
336+ Workspaces json.RawMessage `json:"workspaces"`
337+ }
338+
339+ type bunWorkspaces struct {
340+ Catalog map [string ]string `json:"catalog"`
341+ Catalogs map [string ]map [string ]string `json:"catalogs"`
342+ }
343+
323344func resolveInstallPackages (install map [string ]string ) []string {
324345 result := make ([]string , 0 , len (install ))
325346 for pkg := range install {
@@ -328,12 +349,150 @@ func resolveInstallPackages(install map[string]string) []string {
328349 return result
329350}
330351
331- func resolveInstallVersion (pkg string , install map [string ]string , packageJSON js.PackageJson ) string {
332- if version , ok := install [pkg ]; ok && version != "" && version != "*" {
333- return version
352+ func resolveInstallVersion (pkg string , install map [string ]string , dir string , packageJSON js.PackageJson ) (string , error ) {
353+ version := install [pkg ]
354+ if version == "" || version == "*" {
355+ version = packageJSON .Dependencies [pkg ]
334356 }
335- if version := packageJSON .Dependencies [pkg ]; version != "" {
336- return version
357+ if version == "" {
358+ return "*" , nil
359+ }
360+ if strings .HasPrefix (version , "catalog:" ) {
361+ var err error
362+ version , err = resolveCatalogVersion (dir , pkg , version )
363+ if err != nil {
364+ return "" , err
365+ }
366+ }
367+ for _ , prefix := range []string {"catalog:" , "workspace:" , "file:" , "link:" , "portal:" , "patch:" } {
368+ if strings .HasPrefix (version , prefix ) {
369+ return "" , fmt .Errorf ("could not determine an npm-compatible version for %q in nodejs.install: found %q using %q; pin the version explicitly" , pkg , version , prefix )
370+ }
371+ }
372+ return version , nil
373+ }
374+
375+ func resolveCatalogVersion (dir string , pkg string , version string ) (string , error ) {
376+ workspacePath , err := fs .FindUp (dir , "pnpm-workspace.yaml" )
377+ if err == nil {
378+ return resolvePnpmCatalogVersion (workspacePath , pkg , version )
379+ }
380+ resolved , found , err := resolveBunCatalogVersion (dir , pkg , version )
381+ if err != nil {
382+ return "" , err
383+ }
384+ if found {
385+ return resolved , nil
386+ }
387+ return "" , fmt .Errorf ("could not determine an npm-compatible version for %q in nodejs.install: found %q but pnpm-workspace.yaml was not found and no Bun catalog was found in an ancestor package.json; pin the version explicitly" , pkg , version )
388+ }
389+
390+ func resolvePnpmCatalogVersion (workspacePath string , pkg string , version string ) (string , error ) {
391+ data , err := os .ReadFile (workspacePath )
392+ if err != nil {
393+ return "" , err
394+ }
395+ var workspace catalogSource
396+ if err := yaml .Unmarshal (data , & workspace ); err != nil {
397+ return "" , err
398+ }
399+ resolved , ok := resolveCatalogEntry (pkg , version , workspace )
400+ if ! ok {
401+ return "" , fmt .Errorf ("could not determine an npm-compatible version for %q in nodejs.install: found %q but no matching catalog entry exists in pnpm-workspace.yaml; pin the version explicitly" , pkg , version )
402+ }
403+ return resolved , nil
404+ }
405+
406+ func resolveBunCatalogVersion (dir string , pkg string , version string ) (string , bool , error ) {
407+ currentDir := dir
408+ for {
409+ packagePath := filepath .Join (currentDir , "package.json" )
410+ data , err := os .ReadFile (packagePath )
411+ if err == nil {
412+ source , found , err := parseBunCatalogSource (data )
413+ if err != nil {
414+ return "" , false , err
415+ }
416+ if found {
417+ resolved , ok := resolveCatalogEntry (pkg , version , source )
418+ if ! ok {
419+ return "" , true , fmt .Errorf ("could not determine an npm-compatible version for %q in nodejs.install: found %q but no matching catalog entry exists in %s; pin the version explicitly" , pkg , version , packagePath )
420+ }
421+ return resolved , true , nil
422+ }
423+ } else if ! os .IsNotExist (err ) {
424+ return "" , false , err
425+ }
426+
427+ if currentDir == filepath .Dir (currentDir ) {
428+ break
429+ }
430+ currentDir = filepath .Dir (currentDir )
431+ }
432+ return "" , false , nil
433+ }
434+
435+ func parseBunCatalogSource (data []byte ) (catalogSource , bool , error ) {
436+ var manifest bunPackageJSON
437+ if err := json .Unmarshal (data , & manifest ); err != nil {
438+ return catalogSource {}, false , err
439+ }
440+ source := catalogSource {
441+ Catalog : manifest .Catalog ,
442+ Catalogs : manifest .Catalogs ,
443+ }
444+ workspaceSource , found , err := parseBunWorkspacesCatalogSource (manifest .Workspaces )
445+ if err != nil {
446+ return catalogSource {}, false , err
447+ }
448+ if found {
449+ if workspaceSource .Catalog != nil {
450+ source .Catalog = workspaceSource .Catalog
451+ }
452+ if workspaceSource .Catalogs != nil {
453+ if source .Catalogs == nil {
454+ source .Catalogs = map [string ]map [string ]string {}
455+ }
456+ for name , catalog := range workspaceSource .Catalogs {
457+ source .Catalogs [name ] = catalog
458+ }
459+ }
460+ }
461+ if source .Catalog == nil && len (source .Catalogs ) == 0 {
462+ return catalogSource {}, false , nil
463+ }
464+ return source , true , nil
465+ }
466+
467+ func parseBunWorkspacesCatalogSource (raw json.RawMessage ) (catalogSource , bool , error ) {
468+ trimmed := strings .TrimSpace (string (raw ))
469+ if trimmed == "" || trimmed [0 ] != '{' {
470+ return catalogSource {}, false , nil
471+ }
472+ var workspaces bunWorkspaces
473+ if err := json .Unmarshal (raw , & workspaces ); err != nil {
474+ return catalogSource {}, false , err
475+ }
476+ if workspaces .Catalog == nil && len (workspaces .Catalogs ) == 0 {
477+ return catalogSource {}, false , nil
478+ }
479+ return catalogSource {
480+ Catalog : workspaces .Catalog ,
481+ Catalogs : workspaces .Catalogs ,
482+ }, true , nil
483+ }
484+
485+ func resolveCatalogEntry (pkg string , version string , source catalogSource ) (string , bool ) {
486+ catalogName := strings .TrimSpace (strings .TrimPrefix (version , "catalog:" ))
487+ var catalog map [string ]string
488+ if catalogName == "" || catalogName == "default" {
489+ catalog = source .Catalog
490+ if catalog == nil {
491+ catalog = source .Catalogs ["default" ]
492+ }
493+ } else {
494+ catalog = source .Catalogs [catalogName ]
337495 }
338- return "*"
496+ resolved := catalog [pkg ]
497+ return resolved , resolved != ""
339498}
0 commit comments