cmd/nodejs/firebasenextjs/main.go (108 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/firebasenextjs buildpack. // The nodejs/firebasenextjs buildpack does some prep work for nextjs and overwrites the build script. package main import ( "github.com/GoogleCloudPlatform/buildpacks/pkg/firebase/apphostingschema" "github.com/GoogleCloudPlatform/buildpacks/pkg/firebase/faherror" "github.com/GoogleCloudPlatform/buildpacks/pkg/firebase/util" "github.com/GoogleCloudPlatform/buildpacks/pkg/nodejs" "github.com/Masterminds/semver" "github.com/GoogleCloudPlatform/buildpacks/pkg/env" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" ) const ( // frameworkVersion is the version of next that the application is using frameworkVersion = "FRAMEWORK_VERSION" ) var ( // minNextVersion is the lowest version of nextjs supported by the firebasenextjs buildpack. minNextVersion = semver.MustParse("13.0.0") ) func main() { gcp.Main(detectFn, buildFn) } func detectFn(ctx *gcp.Context) (gcp.DetectResult, error) { appDir := util.ApplicationDirectory(ctx) if !env.IsFAH() { return gcp.OptOut("not a firebase apphosting application"), nil } nodeDeps, err := nodejs.ReadNodeDependencies(ctx, appDir) if err != nil { return nil, err } apphostingSchema, err := apphostingschema.ReadAndValidateFromFile(nodejs.ApphostingPreprocessedPathForPack) if err != nil { return nil, err } if nodejs.HasApphostingPackageOrYamlBuild(nodeDeps.PackageJSON, apphostingSchema) { return gcp.OptOut("apphosting build script found"), nil } supportedNextConfigFiles := []string{"next.config.js", "next.config.mjs", "next.config.ts"} for _, configFile := range supportedNextConfigFiles { exists, err := ctx.FileExists(appDir, configFile) if err != nil { return nil, err } if exists { return gcp.OptInFileFound(configFile), nil } } version, err := nodejs.Version(nodeDeps, "next") if err != nil { ctx.Warnf("Error parsing version from lock file, defaulting to package.json version") version = nodeDeps.PackageJSON.Dependencies["next"] } if version != "" { return gcp.OptIn("nextjs dependency found"), nil } return gcp.OptOut("nextjs config or dependency not found"), nil } func buildFn(ctx *gcp.Context) error { appDir := util.ApplicationDirectory(ctx) nodeDeps, err := nodejs.ReadNodeDependencies(ctx, appDir) if err != nil { return err } if nodeDeps.LockfilePath == "" { return gcp.UserErrorf("%w", faherror.MissingLockFileError(appDir)) } version, err := nodejs.Version(nodeDeps, "next") if err != nil { ctx.Warnf("Error parsing version from lock file, defaulting to package.json version") version = nodeDeps.PackageJSON.Dependencies["next"] } err = validateVersion(ctx, version) if err != nil { return err } // TODO(b/357644160) We we should consider adding a validation step to double check that the adapter version works for the framework version. if version, exists := nodeDeps.PackageJSON.Dependencies["@apphosting/adapter-nextjs"]; exists { ctx.Logf("*** You already have @apphosting/adapter-nextjs@%s listed as a dependency, skipping installation ***", version) ctx.Logf("*** Your package.json build command will be run as is, please make sure it is set to apphosting-adapter-nextjs-build if you intend to build your app using the adapter ***") return nil } buildScript, exists := nodeDeps.PackageJSON.Scripts["build"] if exists && buildScript != "next build" && buildScript != "apphosting-adapter-nextjs-build" { ctx.Warnf("*** You are using a custom build command (your build command is NOT 'next build'), we will accept it as is but will error if output structure is not as expected ***") } njsl, err := ctx.Layer("npm_modules", gcp.BuildLayer, gcp.CacheLayer) if err != nil { return err } err = nodejs.InstallNextJsBuildAdaptor(ctx, njsl, version) if err != nil { return err } // pass nextjs version as environment variable that will configure the build for version matching njsl.BuildEnvironment.Override(frameworkVersion, version) // This env var indicates to the package manager buildpack that a different command needs to be run nodejs.OverrideNextjsBuildScript(njsl) return nil } func validateVersion(ctx *gcp.Context, depVersion string) error { version, err := semver.NewVersion(depVersion) // This should only happen in the case of an unexpected lockfile format, i.e. If there is a breaking update to a lock file schema if err != nil { ctx.Warnf("Unrecognized version of next: %s", depVersion) ctx.Warnf("Consider updating your next dependencies to >=%s", minNextVersion.String()) return nil } if version.LessThan(minNextVersion) { ctx.Warnf("Unsupported version of next: %s", depVersion) ctx.Warnf("Update the next dependencies to >=%s", minNextVersion.String()) return gcp.UserErrorf("%w", faherror.UnsupportedFrameworkVersionError("next", depVersion)) } return nil }