cmd/nodejs/npm/main.go (208 lines of code) (raw):

// Copyright 2020 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/npm buildpack. // The npm buildpack installs dependencies using npm. package main import ( "fmt" "os" "path/filepath" "strings" "github.com/GoogleCloudPlatform/buildpacks/pkg/ar" "github.com/GoogleCloudPlatform/buildpacks/pkg/buildermetrics" "github.com/GoogleCloudPlatform/buildpacks/pkg/cache" "github.com/GoogleCloudPlatform/buildpacks/pkg/devmode" "github.com/GoogleCloudPlatform/buildpacks/pkg/firebase/faherror" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" "github.com/GoogleCloudPlatform/buildpacks/pkg/nodejs" ) const ( cacheTag = "prod dependencies" ) 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 } return gcp.OptInFileFound("package.json"), nil } func buildFn(ctx *gcp.Context) error { ml, err := ctx.Layer("npm_modules", gcp.BuildLayer, gcp.CacheLayer) if err != nil { return fmt.Errorf("creating layer: %w", err) } nm := filepath.Join(ml.Path, "node_modules") if nmExists, _ := ctx.FileExists("node_modules"); nmExists { buildermetrics.GlobalBuilderMetrics().GetCounter(buildermetrics.NpmNodeModulesCounterID).Increment(1) } vendorNpmDeps := nodejs.IsUsingVendoredDependencies() if !vendorNpmDeps { if err := ctx.RemoveAll("node_modules"); err != nil { return err } } if err := ar.GenerateNPMConfig(ctx); err != nil { return fmt.Errorf("generating Artifact Registry credentials: %w", err) } pjs, err := nodejs.ReadPackageJSONIfExists(ctx.ApplicationRoot()) if err != nil { return err } if err := upgradeNPM(ctx, pjs); err != nil { vendorError := "" if vendorNpmDeps { vendorError = "Vendored dependencies detected, please remove the npm version from your package.json to avoid installing npm and instead use the bundled npm" } return fmt.Errorf("%s Error: %w", vendorError, err) } lockfile, err := nodejs.EnsureLockfile(ctx) if err != nil { return err } pjs, err = nodejs.OverrideAppHostingBuildScript(ctx, nodejs.ApphostingPreprocessedPathForPack) if err != nil { return err } buildCmds, isCustomBuild := nodejs.DetermineBuildCommands(pjs, "npm") // 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 } } if vendorNpmDeps { buildermetrics.GlobalBuilderMetrics().GetCounter(buildermetrics.NpmVendorDependenciesCounterID).Increment(1) if _, err := ctx.Exec([]string{"npm", "rebuild"}, gcp.WithEnv("NODE_ENV="+buildNodeEnv), gcp.WithUserAttribution); err != nil { return err } } else { cached, err := nodejs.CheckOrClearCache(ctx, ml, cache.WithStrings(buildNodeEnv), cache.WithFiles("package.json", lockfile)) if err != nil { return fmt.Errorf("checking cache: %w", err) } if cached { // Restore cached node_modules. if _, err := ctx.Exec([]string{"cp", "--archive", nm, "node_modules"}, gcp.WithUserTimingAttribution); err != nil { return err } // Always run npm install to run preinstall/postinstall scripts. // Otherwise it should be a no-op because the lockfile is unchanged. if _, err := ctx.Exec([]string{"npm", "install", "--quiet"}, gcp.WithEnv("NODE_ENV="+buildNodeEnv), gcp.WithUserAttribution); err != nil { return err } } else { ctx.Logf("Installing application dependencies.") installCmd, err := nodejs.NPMInstallCommand(ctx) if err != nil { return err } if _, err := ctx.Exec([]string{"npm", installCmd, "--quiet", "--no-fund", "--no-audit"}, gcp.WithEnv("NODE_ENV="+buildNodeEnv), gcp.WithUserAttribution); err != nil { return err } // Ensure node_modules exists even if no dependencies were installed. if err := ctx.MkdirAll("node_modules", 0755); err != nil { return err } if _, err := ctx.Exec([]string{"cp", "--archive", "node_modules", nm}, gcp.WithUserTimingAttribution); err != nil { return 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 { execOpts := []gcp.ExecOption{gcp.WithUserAttribution} if nodejs.DetectSvelteKitAutoAdapter(pjs) { execOpts = append(execOpts, gcp.WithEnv(nodejs.SvelteAdapterEnv)) } split := strings.Split(cmd, " ") if _, err := ctx.Exec(split, execOpts...); err != nil { if !isCustomBuild { return fmt.Errorf(`%w NOTE: Running the default build script can be skipped by passing the empty environment variable "%s=" to the build`, err, nodejs.GoogleNodeRunScriptsEnv) } 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 } } shouldPrune, err := shouldPrune(ctx, pjs) if err != nil { return err } if shouldPrune { // npm prune deletes devDependencies from node_modules if _, err := ctx.Exec([]string{"npm", "prune", "--production"}, gcp.WithUserAttribution); err != nil { return err } } } el, err := ctx.Layer("env", gcp.BuildLayer, gcp.LaunchLayer) if err != nil { return fmt.Errorf("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. cmd, err := nodejs.DefaultStartCommand(ctx, pjs) if err != nil { return fmt.Errorf("detecting start command: %w", err) } if !devmode.Enabled(ctx) { ctx.AddWebProcess(cmd) return nil } // Configure the entrypoint and metadata for dev mode. if err := devmode.AddFileWatcherProcess(ctx, devmode.Config{ RunCmd: cmd, Ext: devmode.NodeWatchedExtensions, }); err != nil { return fmt.Errorf("adding devmode file watcher: %w", err) } return nil } func shouldPrune(ctx *gcp.Context, pjs *nodejs.PackageJSON) (bool, error) { // if we are vendoring dependencies, we do not need to prune if nodejs.IsUsingVendoredDependencies() { return false, nil } // if there are no devDependencies, there is no need to prune. if !nodejs.HasDevDependencies(pjs) { return false, nil } if nodeEnv := nodejs.NodeEnv(); nodeEnv != nodejs.EnvProduction { ctx.Logf("Retaining devDependencies because $NODE_ENV=%q.", nodeEnv) return false, nil } canPrune, err := nodejs.SupportsNPMPrune(ctx) if err == nil && !canPrune { ctx.Warnf("Retaining devDependencies because the version of NPM you are using does not support 'npm prune'.") } return canPrune, err } func upgradeNPM(ctx *gcp.Context, pjs *nodejs.PackageJSON) error { npmVersion, err := nodejs.RequestedNPMVersion(pjs) if err != nil { return err } if npmVersion == "" { // if an NPM version was not requested, use whatever was bundled with Node.js. return nil } npmLayer, err := ctx.Layer("npm", gcp.BuildLayer, gcp.LaunchLayer, gcp.CacheLayer) if err != nil { return fmt.Errorf("creating layer: %w", err) } metaVersion := ctx.GetMetadata(npmLayer, "version") if metaVersion == npmVersion { ctx.Logf("npm@%s cache hit, skipping installation.", npmVersion) return nil } ctx.ClearLayer(npmLayer) prefix := fmt.Sprintf("--prefix=%s", npmLayer.Path) pkg := fmt.Sprintf("npm@%s", npmVersion) if _, err := ctx.Exec([]string{"npm", "install", "-g", prefix, pkg}, gcp.WithUserAttribution); err != nil { return err } // Set the path here to ensure the version we just installed takes precedence over the npm bundled // with the Node.js engine. if err := ctx.Setenv("PATH", filepath.Join(npmLayer.Path, "bin")+":"+os.Getenv("PATH")); err != nil { return err } return nil }