Skip to content
Merged
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
173 changes: 166 additions & 7 deletions pkg/runtime/node/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/sst/sst/v3/pkg/js"
"github.com/sst/sst/v3/pkg/process"
"github.com/sst/sst/v3/pkg/runtime"
"gopkg.in/yaml.v3"
)

var forceExternal = []string{
Expand Down Expand Up @@ -273,7 +274,11 @@ func (r *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim
}
dependencies := map[string]string{}
for _, pkg := range installPackages {
dependencies[pkg] = resolveInstallVersion(pkg, properties.Install, parsed)
version, err := resolveInstallVersion(pkg, properties.Install, filepath.Dir(src), parsed)
if err != nil {
return nil, err
}
dependencies[pkg] = version
}
outPkg := filepath.Join(input.Out(), "package.json")
outFile, err := os.Create(outPkg)
Expand Down Expand Up @@ -320,6 +325,22 @@ func (r *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim
}, nil
}

type catalogSource struct {
Catalog map[string]string `json:"catalog" yaml:"catalog"`
Catalogs map[string]map[string]string `json:"catalogs" yaml:"catalogs"`
}

type bunPackageJSON struct {
Catalog map[string]string `json:"catalog"`
Catalogs map[string]map[string]string `json:"catalogs"`
Workspaces json.RawMessage `json:"workspaces"`
}

type bunWorkspaces struct {
Catalog map[string]string `json:"catalog"`
Catalogs map[string]map[string]string `json:"catalogs"`
}

func resolveInstallPackages(install map[string]string) []string {
result := make([]string, 0, len(install))
for pkg := range install {
Expand All @@ -328,12 +349,150 @@ func resolveInstallPackages(install map[string]string) []string {
return result
}

func resolveInstallVersion(pkg string, install map[string]string, packageJSON js.PackageJson) string {
if version, ok := install[pkg]; ok && version != "" && version != "*" {
return version
func resolveInstallVersion(pkg string, install map[string]string, dir string, packageJSON js.PackageJson) (string, error) {
version := install[pkg]
if version == "" || version == "*" {
version = packageJSON.Dependencies[pkg]
}
if version := packageJSON.Dependencies[pkg]; version != "" {
return version
if version == "" {
return "*", nil
}
if strings.HasPrefix(version, "catalog:") {
var err error
version, err = resolveCatalogVersion(dir, pkg, version)
if err != nil {
return "", err
}
}
for _, prefix := range []string{"catalog:", "workspace:", "file:", "link:", "portal:", "patch:"} {
if strings.HasPrefix(version, prefix) {
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)
}
}
return version, nil
}

func resolveCatalogVersion(dir string, pkg string, version string) (string, error) {
workspacePath, err := fs.FindUp(dir, "pnpm-workspace.yaml")
if err == nil {
return resolvePnpmCatalogVersion(workspacePath, pkg, version)
}
resolved, found, err := resolveBunCatalogVersion(dir, pkg, version)
if err != nil {
return "", err
}
if found {
return resolved, nil
}
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)
}

func resolvePnpmCatalogVersion(workspacePath string, pkg string, version string) (string, error) {
data, err := os.ReadFile(workspacePath)
if err != nil {
return "", err
}
var workspace catalogSource
if err := yaml.Unmarshal(data, &workspace); err != nil {
return "", err
}
resolved, ok := resolveCatalogEntry(pkg, version, workspace)
if !ok {
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)
}
return resolved, nil
}

func resolveBunCatalogVersion(dir string, pkg string, version string) (string, bool, error) {
currentDir := dir
for {
packagePath := filepath.Join(currentDir, "package.json")
data, err := os.ReadFile(packagePath)
if err == nil {
source, found, err := parseBunCatalogSource(data)
if err != nil {
return "", false, err
}
if found {
resolved, ok := resolveCatalogEntry(pkg, version, source)
if !ok {
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)
}
return resolved, true, nil
}
} else if !os.IsNotExist(err) {
return "", false, err
}

if currentDir == filepath.Dir(currentDir) {
break
}
currentDir = filepath.Dir(currentDir)
}
return "", false, nil
}

func parseBunCatalogSource(data []byte) (catalogSource, bool, error) {
var manifest bunPackageJSON
if err := json.Unmarshal(data, &manifest); err != nil {
return catalogSource{}, false, err
}
source := catalogSource{
Catalog: manifest.Catalog,
Catalogs: manifest.Catalogs,
}
workspaceSource, found, err := parseBunWorkspacesCatalogSource(manifest.Workspaces)
if err != nil {
return catalogSource{}, false, err
}
if found {
if workspaceSource.Catalog != nil {
source.Catalog = workspaceSource.Catalog
}
if workspaceSource.Catalogs != nil {
if source.Catalogs == nil {
source.Catalogs = map[string]map[string]string{}
}
for name, catalog := range workspaceSource.Catalogs {
source.Catalogs[name] = catalog
}
}
}
if source.Catalog == nil && len(source.Catalogs) == 0 {
return catalogSource{}, false, nil
}
return source, true, nil
}

func parseBunWorkspacesCatalogSource(raw json.RawMessage) (catalogSource, bool, error) {
trimmed := strings.TrimSpace(string(raw))
if trimmed == "" || trimmed[0] != '{' {
return catalogSource{}, false, nil
}
var workspaces bunWorkspaces
if err := json.Unmarshal(raw, &workspaces); err != nil {
return catalogSource{}, false, err
}
if workspaces.Catalog == nil && len(workspaces.Catalogs) == 0 {
return catalogSource{}, false, nil
}
return catalogSource{
Catalog: workspaces.Catalog,
Catalogs: workspaces.Catalogs,
}, true, nil
}

func resolveCatalogEntry(pkg string, version string, source catalogSource) (string, bool) {
catalogName := strings.TrimSpace(strings.TrimPrefix(version, "catalog:"))
var catalog map[string]string
if catalogName == "" || catalogName == "default" {
catalog = source.Catalog
if catalog == nil {
catalog = source.Catalogs["default"]
}
} else {
catalog = source.Catalogs[catalogName]
}
return "*"
resolved := catalog[pkg]
return resolved, resolved != ""
}
Loading
Loading