cmd/nodejs/pnpm/main.go (105 lines of code) (raw):
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Implements nodejs/pnpm buildpack.
// The pnpm buildpack installs dependencies using pnpm and installs pnpm itself if not present.
package main
import (
"os"
"path/filepath"
"strings"
"github.com/GoogleCloudPlatform/buildpacks/pkg/env"
"github.com/GoogleCloudPlatform/buildpacks/pkg/firebase/faherror"
gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack"
"github.com/GoogleCloudPlatform/buildpacks/pkg/nodejs"
)
const (
cacheTag = "prod dependencies"
pnpmLayer = "pnpm_engine"
)
func main() {
gcp.Main(detectFn, buildFn)
}
func detectFn(ctx *gcp.Context) (gcp.DetectResult, error) {
pkgJSONExists, err := ctx.FileExists("package.json")
if err != nil {
return nil, err
}
if !pkgJSONExists {
return gcp.OptOutFileNotFound("package.json"), nil
}
pnpmLockExists, err := ctx.FileExists(nodejs.PNPMLock)
if err != nil {
return nil, err
}
if !pnpmLockExists {
return gcp.OptOutFileNotFound(nodejs.PNPMLock), nil
}
return gcp.OptIn("found pnpm-lock.yaml and package.json"), nil
}
func buildFn(ctx *gcp.Context) error {
pjs, err := nodejs.ReadPackageJSONIfExists(ctx.ApplicationRoot())
if err != nil {
return err
}
if err := installPNPM(ctx, pjs); err != nil {
return gcp.InternalErrorf("installing pnpm: %w", err)
}
if err := pnpmInstallModules(ctx, pjs); err != nil {
return err
}
el, err := ctx.Layer("env", gcp.BuildLayer, gcp.LaunchLayer)
if err != nil {
return gcp.InternalErrorf("creating layer: %w", err)
}
el.SharedEnvironment.Prepend("PATH", string(os.PathListSeparator), filepath.Join(ctx.ApplicationRoot(), "node_modules", ".bin"))
el.SharedEnvironment.Default("NODE_ENV", nodejs.NodeEnv())
// Configure the entrypoint for production.
ctx.AddWebProcess([]string{"pnpm", "run", "start"})
return nil
}
func pnpmInstallModules(ctx *gcp.Context, pjs *nodejs.PackageJSON) error {
pjs, err := nodejs.OverrideAppHostingBuildScript(ctx, nodejs.ApphostingPreprocessedPathForPack)
if err != nil {
return err
}
buildCmds, _ := nodejs.DetermineBuildCommands(pjs, "pnpm")
// Respect the user's NODE_ENV value if it's set
buildNodeEnv, nodeEnvPresent := os.LookupEnv(nodejs.EnvNodeEnv)
if !nodeEnvPresent {
if len(buildCmds) > 0 {
// Assume that dev dependencies are required to run build scripts to
// support the most use cases possible.
buildNodeEnv = nodejs.EnvDevelopment
} else {
buildNodeEnv = nodejs.EnvProduction
}
}
cmd := []string{"pnpm", "install"}
if _, err := ctx.Exec(cmd, gcp.WithUserAttribution, gcp.WithEnv("CI=true"), gcp.WithEnv("NODE_ENV="+buildNodeEnv)); err != nil {
return gcp.UserErrorf("installing pnpm dependencies: %w", err)
}
if len(buildCmds) > 0 {
// If there are multiple build scripts to run, run them one-by-one so the logs are
// easier to understand.
for _, cmd := range buildCmds {
split := strings.Split(cmd, " ")
if _, err := ctx.Exec(split, gcp.WithUserAttribution); err != nil {
if fahCmd, fahCmdPresent := os.LookupEnv(nodejs.AppHostingBuildEnv); fahCmdPresent {
return gcp.UserErrorf("%w", faherror.FailedFrameworkBuildError(fahCmd, err))
}
if nodejs.HasApphostingPackageBuild(pjs) {
return gcp.UserErrorf("%w", faherror.FailedFrameworkBuildError(pjs.Scripts[nodejs.ScriptApphostingBuild], err))
}
return err
}
}
}
shouldPruneDevDependencies := buildNodeEnv == nodejs.EnvDevelopment && !nodeEnvPresent && nodejs.HasDevDependencies(pjs)
if shouldPruneDevDependencies {
if env.IsFAH() {
// We don't prune if the user is using App Hosting since App Hosting builds don't
// rely on the node_modules folder at this point.
return nil
}
// If we installed dependencies with NODE_ENV=development and the user didn't explicitly set
// NODE_ENV we should prune the devDependencies from the final app image.
cmd := []string{"pnpm", "prune", "--prod"}
if _, err := ctx.Exec(cmd, gcp.WithUserAttribution, gcp.WithEnv("CI=true")); err != nil {
return gcp.UserErrorf("pruning devDependencies: %w", err)
}
}
return nil
}
func installPNPM(ctx *gcp.Context, pjs *nodejs.PackageJSON) error {
layer, err := ctx.Layer(pnpmLayer, gcp.BuildLayer, gcp.CacheLayer, gcp.LaunchLayer)
if err != nil {
return gcp.InternalErrorf("creating %v layer: %w", pnpmLayer, err)
}
return nodejs.InstallPNPM(ctx, layer, pjs)
}