cmd/nodejs/firebaseangular/main.go (106 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/firebaseangular buildpack.
// The nodejs/firebaseangular buildpack does some prep work for angular and runs the build script.
package main
import (
"github.com/GoogleCloudPlatform/buildpacks/pkg/nodejs"
"github.com/Masterminds/semver"
"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/env"
gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack"
)
const (
// frameworkVersion is the version of angular that the application is using
frameworkVersion = "FRAMEWORK_VERSION"
)
var (
// minAngularVersion is the lowest version of angular supported by the firebase angular buildpack.
minAngularVersion = semver.MustParse("17.2.0")
)
func main() {
gcp.Main(detectFn, buildFn)
}
func detectFn(ctx *gcp.Context) (gcp.DetectResult, error) {
if !env.IsFAH() {
return gcp.OptOut("not a firebase apphosting application"), nil
}
appDir := util.ApplicationDirectory(ctx)
angularJSONExists, err := ctx.FileExists(appDir, "angular.json")
if err != nil {
return nil, err
}
if angularJSONExists {
return gcp.OptInFileFound("angular.json"), nil
}
// Some Angular project configurations don't require an angular.json file (e.g. Nx projects).
// In these cases, we check if the angular builder is specified as a dependency.
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
}
version, err := nodejs.Version(nodeDeps, "@angular-devkit/build-angular")
if err != nil {
ctx.Warnf("Error parsing version from lock file, defaulting to package.json version")
if nodeDeps.PackageJSON.DevDependencies["@angular-devkit/build-angular"] != "" {
return gcp.OptIn("angular builder dependency found"), nil
}
return gcp.OptOut("angular builder dependency not found"), err
}
if version != "" {
return gcp.OptIn("angular builder dependency found"), nil
}
return gcp.OptOut("angular builder 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))
}
// Ensure that the right version of the application builder is installed.
builderVersion, err := nodejs.Version(nodeDeps, "@angular-devkit/build-angular")
if err != nil {
ctx.Warnf("Error parsing version from lock file, defaulting to package.json version")
builderVersion = nodeDeps.PackageJSON.DevDependencies["@angular-devkit/build-angular"]
}
err = validateVersion(ctx, builderVersion)
if err != nil {
return err
}
// TODO(b/357644160) 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-angular"]; exists {
ctx.Logf("*** You already have @apphosting/adapter-angular@%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-angular-build if you intend to build your app using the adapter ***")
return nil
}
buildScript, exists := nodeDeps.PackageJSON.Scripts["build"]
if exists && buildScript != "ng build" && buildScript != "apphosting-adapter-angular-build" {
ctx.Warnf("*** You are using a custom build command (your build command is NOT 'ng build'), we will accept it as is but will error if output structure is not as expected ***")
}
al, err := ctx.Layer("npm_modules", gcp.BuildLayer, gcp.CacheLayer)
if err != nil {
return err
}
if err = nodejs.InstallAngularBuildAdaptor(ctx, al, builderVersion); err != nil {
return err
}
// pass angular version as environment variable that will configure the build for version matching
al.BuildEnvironment.Override(frameworkVersion, builderVersion)
// This env var indicates to the package manager buildpack that a different command needs to be run
nodejs.OverrideAngularBuildScript(al)
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 angular: %s", depVersion)
ctx.Warnf("Consider updating your angular dependencies to >=%s", minAngularVersion.String())
return nil
}
if version.LessThan(minAngularVersion) {
ctx.Warnf("Update the angular dependencies to >=%s", minAngularVersion.String())
return gcp.UserErrorf("%w", faherror.UnsupportedFrameworkVersionError("angular", depVersion))
}
return nil
}