pkg/nodejs/npm.go (165 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. package nodejs import ( "fmt" "os" "strings" "github.com/GoogleCloudPlatform/buildpacks/pkg/buildermetrics" "github.com/GoogleCloudPlatform/buildpacks/pkg/env" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" "github.com/Masterminds/semver" ) const ( // PackageLock is the name of the npm lock file. PackageLock = "package-lock.json" // NPMShrinkwrap is the name of the npm shrinkwrap file. NPMShrinkwrap = "npm-shrinkwrap.json" // GoogleNodeRunScriptsEnv is the env var that can be used to configure a list of package.json // scripts that should be run during the build process. GoogleNodeRunScriptsEnv = "GOOGLE_NODE_RUN_SCRIPTS" // nodejsNPMBuildEnv is an env var that enables running `npm run build` by default. nodejsNPMBuildEnv = "GOOGLE_EXPERIMENTAL_NODEJS_NPM_BUILD_ENABLED" // VendorNpmDeps for vendoring npm dependencies VendorNpmDeps = "GOOGLE_VENDOR_NPM_DEPENDENCIES" // AppHostingBuildEnv is the env var that contains the build command to run for Firebase backends. AppHostingBuildEnv = "APPHOSTING_BUILD" ) var ( // minPruneVersion is the first npm version that supports the prune command. minPruneVersion = semver.MustParse("5.7.0") // minNpmCIVersion is the first npm version that suports the ci command. minNpmCIVersion = semver.MustParse("6.14.0") ) // RequestedNPMVersion returns any customer provided NPM version constraint configured in the // "engines" section of the package.json file in the given application dir. func RequestedNPMVersion(pjs *PackageJSON) (string, error) { if pjs == nil || pjs.Engines.NPM == "" { return "", nil } version, err := resolvePackageVersion("npm", pjs.Engines.NPM) if err != nil { gcp.InternalErrorf("fetching npm metadata: %v", err) } return version, nil } // EnsureLockfile returns the name of the lockfile, generating a package-lock.json if necessary. func EnsureLockfile(ctx *gcp.Context) (string, error) { npmShrinkwrapExists, err := ctx.FileExists(NPMShrinkwrap) if err != nil { return "", err } // npm prefers npm-shrinkwrap.json, see https://docs.npmjs.com/cli/shrinkwrap. if npmShrinkwrapExists { return NPMShrinkwrap, nil } pkgLockExists, err := ctx.FileExists(PackageLock) if err != nil { return "", err } if !pkgLockExists { ctx.Logf("Generating %s.", PackageLock) ctx.Warnf("*** Improve build performance by generating and committing %s.", PackageLock) if _, err := ctx.Exec([]string{"npm", "install", "--package-lock-only", "--quiet"}, gcp.WithUserAttribution); err != nil { return "", err } } return PackageLock, nil } // NPMInstallCommand returns the correct install command based on the version of Node.js. By default // we prefer "npm ci" because it handles transitive dependencies determinstically. See the NPM docs: // https://docs.npmjs.com/cli/v6/commands/npm-ci func NPMInstallCommand(ctx *gcp.Context) (string, error) { // b/236758688: For backwards compatibility on GAE & GCF Node.js 10 and older, always use `npm install`. if env.IsGAE() || env.IsGCF() { isOldNode, err := isPreNode11(ctx) if err != nil { return "", err } if isOldNode { return "install", nil } } npmVer, err := npmVersion(ctx) if err != nil { return "", err } version, err := semver.NewVersion(npmVer) if err != nil { return "", gcp.InternalErrorf("parsing npm version: %v", err) } // HACK: For backwards compatibility with old versions of npm always use `npm install`. if version.LessThan(minNpmCIVersion) { return "install", nil } return "ci", nil } // npmVersion returns the version of NPM installed in the system. var npmVersion = func(ctx *gcp.Context) (string, error) { result, err := ctx.Exec([]string{"npm", "--version"}) if err != nil { return "", nil } return strings.TrimSpace(result.Stdout), nil } // SupportsNPMPrune returns true if the version of npm installed in the system supports the prune // command. func SupportsNPMPrune(ctx *gcp.Context) (bool, error) { npmVer, err := npmVersion(ctx) if err != nil { return false, err } version, err := semver.NewVersion(npmVer) if err != nil { return false, gcp.InternalErrorf("parsing npm version: %v", err) } return !version.LessThan(minPruneVersion), nil } // DetermineBuildCommands returns a list of "npm run" commands to be executed during the build // and a bool representing whether this is a "custom build" (user-specified build scripts) // or a system build step (default build behavior). // // Users can specify npm scripts with the following order of precedence: // 1. "apphosting:build" script in package.json // 2. APPHOSTING_BUILD env var // 3. GOOGLE_NODE_RUN_SCRIPTS env var // 4. "gcp-build" script in package.json // 5. "build" script in package.json func DetermineBuildCommands(pjs *PackageJSON, pkgTool string) (cmds []string, isCustomBuild bool) { if HasApphostingPackageBuild(pjs) { return []string{runCommand(pkgTool, "apphosting:build")}, true } appHostingBuildScript, appHostingBuildScriptPresent := os.LookupEnv(AppHostingBuildEnv) if appHostingBuildScriptPresent { return []string{appHostingBuildScript}, true } envScript, envScriptPresent := os.LookupEnv(GoogleNodeRunScriptsEnv) if envScriptPresent { buildermetrics.GlobalBuilderMetrics().GetCounter(buildermetrics.NpmGoogleNodeRunScriptsUsageCounterID).Increment(1) // Setting `GOOGLE_NODE_RUN_SCRIPTS=` preserves legacy behavior where "npm run build" was NOT // run, even though "build" was provided. if strings.TrimSpace(envScript) == "" { return []string{}, true } scripts := strings.Split(envScript, ",") for _, s := range scripts { cmds = append(cmds, runCommand(pkgTool, s)) } return cmds, true } if HasGCPBuild(pjs) { buildermetrics.GlobalBuilderMetrics().GetCounter(buildermetrics.NpmGcpBuildUsageCounterID).Increment(1) if gcpBuild := pjs.Scripts[ScriptGCPBuild]; strings.TrimSpace(gcpBuild) == "" { return []string{}, true } return []string{runCommand(pkgTool, "gcp-build")}, true } if HasScript(pjs, ScriptBuild) { buildermetrics.GlobalBuilderMetrics().GetCounter(buildermetrics.NpmBuildUsageCounterID).Increment(1) if build := pjs.Scripts[ScriptBuild]; strings.TrimSpace(build) == "" { return []string{}, false } return []string{runCommand(pkgTool, "build")}, false } return []string{}, false } // IsUsingVendoredDependencies returns true if the builder should be using the vendored dependencies. func IsUsingVendoredDependencies() bool { val, _ := env.IsPresentAndTrue(VendorNpmDeps) return val } func runCommand(pkgTool, command string) string { return fmt.Sprintf("%s run %s", pkgTool, strings.TrimSpace(command)) } // DefaultStartCommand returns the default command that should be used to configure the application // web process if the user has not explicitly configured one. The algorithm follows the conventions // of Nodejs package.json files: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#main // 1. if script.start is specified return `npm run start` // 2. if the project contains server.js `npm run start` // 3. if main is specified `node ${pjs.main}` // 4. otherwise `node index.js“ func DefaultStartCommand(ctx *gcp.Context, pjs *PackageJSON) ([]string, error) { if pjs == nil { return []string{"node", "index.js"}, nil } if angularStart := ExtractAngularStartCommand(pjs); angularStart != "" { return strings.Fields(angularStart), nil } if _, ok := pjs.Scripts["start"]; ok { return []string{"npm", "run", "start"}, nil } if nuxt, err := NuxtStartCommand(ctx); err != nil || nuxt != nil { return nuxt, err } if svelteKit, err := SvelteKitStartCommand(ctx); err != nil || svelteKit != nil { return svelteKit, err } exists, err := ctx.FileExists(ctx.ApplicationRoot(), "server.js") if err != nil { return nil, err } if exists { return []string{"npm", "run", "start"}, nil } if pjs.Main != "" { return []string{"node", pjs.Main}, nil } return []string{"node", "index.js"}, nil }