11package yarn
22
33import (
4- "fmt"
5- "os"
6- "path/filepath"
7- "strconv"
8- "time"
9-
10- "github.com/paketo-buildpacks/packit/v2"
11- "github.com/paketo-buildpacks/packit/v2/chronos"
12- "github.com/paketo-buildpacks/packit/v2/draft"
13- "github.com/paketo-buildpacks/packit/v2/postal"
14- "github.com/paketo-buildpacks/packit/v2/sbom"
15- "github.com/paketo-buildpacks/packit/v2/scribe"
4+ "fmt"
5+ "os"
6+ "os/exec"
7+ "path/filepath"
8+ "strconv"
9+ "strings"
10+ "time"
11+ "encoding/json"
12+
13+ "github.com/paketo-buildpacks/packit/v2"
14+ "github.com/paketo-buildpacks/packit/v2/chronos"
15+ "github.com/paketo-buildpacks/packit/v2/draft"
16+ "github.com/paketo-buildpacks/packit/v2/postal"
17+ "github.com/paketo-buildpacks/packit/v2/sbom"
18+ "github.com/paketo-buildpacks/packit/v2/scribe"
1619)
1720
1821//go:generate faux --interface DependencyManager --output fakes/dependency_manager.go
@@ -41,13 +44,38 @@ func Build(
4144 return packit.BuildResult {}, err
4245 }
4346
44- planner := draft .NewPlanner ()
45- entry , _ := planner .Resolve ("yarn" , context .Plan .Entries , nil )
46- version , ok := entry .Metadata ["version" ].(string )
47- if ! ok {
48- version = "default"
49- }
50-
47+ planner := draft .NewPlanner ()
48+ entry , _ := planner .Resolve ("yarn" , context .Plan .Entries , nil )
49+ version , ok := entry .Metadata ["version" ].(string )
50+ if ! ok {
51+ version = "default"
52+ }
53+
54+ // Detect Yarn Berry (modern) by presence of .yarnrc.yml/.yarnrc.yaml
55+ isBerryYarn := false
56+ if _ , statErr := os .Stat (filepath .Join (context .WorkingDir , ".yarnrc.yml" )); statErr == nil {
57+ isBerryYarn = true
58+ } else if _ , statErr := os .Stat (filepath .Join (context .WorkingDir , ".yarnrc.yaml" )); statErr == nil {
59+ isBerryYarn = true
60+ }
61+
62+ // If package.json declares packageManager: "yarn@<ver>", prefer that ONLY for Berry projects
63+ var pmYarnVersion string
64+ if data , readErr := os .ReadFile (filepath .Join (context .WorkingDir , "package.json" )); readErr == nil {
65+ var pkg struct {
66+ PackageManager string `json:"packageManager"`
67+ }
68+ if jsonErr := json .Unmarshal (data , & pkg ); jsonErr == nil {
69+ pm := strings .TrimSpace (pkg .PackageManager )
70+ if strings .HasPrefix (pm , "yarn@" ) {
71+ v := strings .TrimPrefix (pm , "yarn@" )
72+ if idx := strings .IndexAny (v , " +#" ); idx != - 1 {
73+ v = v [:idx ]
74+ }
75+ pmYarnVersion = v
76+ }
77+ }
78+ }
5179 dependency , err := dependencyManager .Resolve (
5280 filepath .Join (context .CNBPath , "buildpack.toml" ),
5381 entry .Name ,
@@ -71,8 +99,15 @@ func Build(
7199 launchMetadata = packit.LaunchMetadata {BOM : bom }
72100 }
73101
74- cachedSHA , ok := yarnLayer .Metadata [DependencyCacheKey ].(string )
75- if ok && postal .Checksum (dependency .Checksum ).MatchString (cachedSHA ) {
102+ // Use resolved dependency version as the install version (override with packageManager only for Berry)
103+
104+ resolvedInstallVersion := dependency .Version
105+ if isBerryYarn && pmYarnVersion != "" {
106+ resolvedInstallVersion = pmYarnVersion
107+ }
108+
109+ cachedSHA , ok := yarnLayer .Metadata [DependencyCacheKey ].(string )
110+ if ok && ! isBerryYarn && postal .Checksum (dependency .Checksum ).MatchString (cachedSHA ) {
76111 logger .Process ("Reusing cached layer %s" , yarnLayer .Path )
77112 logger .Break ()
78113
@@ -85,25 +120,63 @@ func Build(
85120 }, nil
86121 }
87122
88- logger .Process ("Executing build process" )
123+ logger .Process ("Executing build process" )
89124
90125 yarnLayer , err = yarnLayer .Reset ()
91126 if err != nil {
92127 return packit.BuildResult {}, err
93128 }
94129
95- yarnLayer .Launch , yarnLayer .Build , yarnLayer .Cache = launch , build , build
96-
97- logger .Subprocess ("Installing Yarn" )
98-
99- duration , err := clock .Measure (func () error {
100- return dependencyManager .Deliver (dependency , context .CNBPath , yarnLayer .Path , context .Platform .Path )
101- })
102- if err != nil {
103- return packit.BuildResult {}, err
104- }
105- logger .Action ("Completed in %s" , duration .Round (time .Millisecond ))
106- logger .Break ()
130+ yarnLayer .Launch , yarnLayer .Build , yarnLayer .Cache = launch , build , build
131+
132+ logger .Subprocess ("Installing Yarn %s" , resolvedInstallVersion )
133+
134+ var duration time.Duration
135+ if isBerryYarn {
136+ // Persist Corepack cache under the yarn layer so the runtime doesn't re-download Yarn
137+ corepackDir := filepath .Join (yarnLayer .Path , "corepack" )
138+ if mkErr := os .MkdirAll (corepackDir , 0o755 ); mkErr != nil {
139+ return packit.BuildResult {}, mkErr
140+ }
141+
142+
143+ duration , err = clock .Measure (func () error {
144+ steps := [][]string {
145+ {"corepack" , "enable" },
146+ {"corepack" , "prepare" , fmt .Sprintf ("yarn@%s" , resolvedInstallVersion ), "--activate" },
147+ }
148+ for _ , args := range steps {
149+
150+ cmd := exec .Command (args [0 ], args [1 :]... )
151+ cmd .Env = append (os .Environ (), "COREPACK_HOME=" + corepackDir )
152+ cmd .Stdout = os .Stdout
153+ cmd .Stderr = os .Stderr
154+ if err := cmd .Run (); err != nil {
155+ return err
156+ }
157+ }
158+ return nil
159+ })
160+ if err != nil {
161+ return packit.BuildResult {}, err
162+ }
163+ logger .Action ("Completed in %s" , duration .Round (time .Millisecond ))
164+ logger .Break ()
165+ // Ensure COREPACK_HOME is present in build and launch images
166+ yarnLayer .Build = true
167+ yarnLayer .Launch = true
168+ yarnLayer .BuildEnv .Default ("COREPACK_HOME" , filepath .Join (yarnLayer .Path , "corepack" ))
169+ yarnLayer .LaunchEnv .Default ("COREPACK_HOME" , filepath .Join (yarnLayer .Path , "corepack" ))
170+ } else {
171+ duration , err = clock .Measure (func () error {
172+ return dependencyManager .Deliver (dependency , context .CNBPath , yarnLayer .Path , context .Platform .Path )
173+ })
174+ if err != nil {
175+ return packit.BuildResult {}, err
176+ }
177+ logger .Action ("Completed in %s" , duration .Round (time .Millisecond ))
178+ logger .Break ()
179+ }
107180
108181 sbomDisabled , err := checkSbomDisabled ()
109182 if err != nil {
@@ -134,9 +207,13 @@ func Build(
134207 }
135208 }
136209
137- yarnLayer .Metadata = map [string ]interface {}{
138- DependencyCacheKey : dependency .Checksum ,
139- }
210+ cacheValue := dependency .Checksum
211+ if isBerryYarn || cacheValue == "" {
212+ cacheValue = dependency .Version
213+ }
214+ yarnLayer .Metadata = map [string ]interface {}{
215+ DependencyCacheKey : cacheValue ,
216+ }
140217
141218 return packit.BuildResult {
142219 Layers : []packit.Layer {yarnLayer },
0 commit comments