コード例 #1
0
ファイル: AppBuilder.cs プロジェクト: bgkyer/LambdaSharpTool
        private void BuildPackage(IApp app, string?gitSha, string?gitBranch, string folder)
        {
            // discover files to package
            var files = new List <KeyValuePair <string, string> >();

            if (Directory.Exists(folder))
            {
                foreach (var filePath in Directory.GetFiles(folder, "*", SearchOption.AllDirectories))
                {
                    var relativeFilePathName = Path.GetRelativePath(folder, filePath);

                    // NOTE (2020-10-18, bjorg): skip 'appsettings.Development.json' file; since it's only useful for running locally and might contain sensitive information
                    if (relativeFilePathName == AppSettingsDevelopmentJsonFileName)
                    {
                        continue;
                    }
                    files.Add(new KeyValuePair <string, string>(relativeFilePathName, filePath));
                }
                files = files.OrderBy(file => file.Key).ToList();
            }
            else
            {
                LogError($"cannot find folder '{folder}'");
                return;
            }

            // compute hash for all files
            var fileValueToFileKey = files.ToDictionary(kv => kv.Value, kv => kv.Key);
            var hash    = files.Select(kv => kv.Value).ComputeHashForFiles(file => fileValueToFileKey[file]);
            var package = Path.Combine(Provider.OutputDirectory, $"app_{Provider.ModuleFullName}_{app.LogicalId}_{hash}.zip");

            // only build package if it doesn't exist
            if (!Provider.ExistingPackages.Remove(package))
            {
                // check appsettings.json file exists
                var appSettingsFilepath = Path.Combine(folder, AppSettingsJsonFileName);
                var appSettingsExists   = File.Exists(appSettingsFilepath);
                if (!appSettingsExists && ((gitSha != null) || (gitBranch != null)))
                {
                    // add fake entry into list of files
                    files.Add(new KeyValuePair <string, string>(AppSettingsJsonFileName, appSettingsFilepath));
                }

                // package contents with built-in zip library
                using (var zipArchive = System.IO.Compression.ZipFile.Open(package, ZipArchiveMode.Create, new ForwardSlashEncoder())) {
                    foreach (var file in files)
                    {
                        if (Provider.DetailedOutput)
                        {
                            Console.WriteLine($"... zipping: {file.Key}");
                        }

                        // check if appsettings.json is being processed
                        if (file.Key == AppSettingsJsonFileName)
                        {
                            var zipEntry = zipArchive.CreateEntry(file.Key);

                            // check if appsettings.json exists
                            string appSettingsText;
                            if (appSettingsExists)
                            {
                                // read original file and attempt to augment it
                                appSettingsText = File.ReadAllText(file.Value);
                                var appSettings = JsonToNativeConverter.ParseObject(appSettingsText);
                                if (appSettings != null)
                                {
                                    AddLambdaSharpSettings(appSettings);
                                    appSettingsText = JsonSerializer.Serialize(appSettings, new JsonSerializerOptions {
                                        IgnoreNullValues = true,
                                        WriteIndented    = false
                                    });
                                }
                            }
                            else
                            {
                                // create default appsettings.json file
                                var appSettings = new Dictionary <string, object?>();
                                AddLambdaSharpSettings(appSettings);
                                appSettingsText = JsonSerializer.Serialize(appSettings, new JsonSerializerOptions {
                                    IgnoreNullValues = true,
                                    WriteIndented    = false
                                });
                            }
                            using (var zipStream = zipEntry.Open()) {
                                zipStream.Write(Encoding.UTF8.GetBytes(appSettingsText));
                            }
                        }
                        else
                        {
                            zipArchive.CreateEntryFromFile(file.Value, file.Key);
                        }
                    }
                }
            }
            else
            {
                // update last write time on file
                File.SetLastWriteTimeUtc(package, DateTime.UtcNow);
            }
            Provider.AddArtifact($"{app.FullName}::PackageName", package);

            // local functions
            void AddLambdaSharpSettings(Dictionary <string, object?> appSettings)
            {
                var lambdaSharpSettings = new Dictionary <string, object?>();

                if (gitSha != null)
                {
                    lambdaSharpSettings["GitSha"] = gitSha;
                }
                if (gitBranch != null)
                {
                    lambdaSharpSettings["GitBranch"] = gitBranch;
                }
                appSettings["LambdaSharp"] = lambdaSharpSettings;
            }
        }
コード例 #2
0
        public async Task CreateInvocationTargetSchemasAsync(
            string directory,
            string rootNamespace,
            IEnumerable <string> methodReferences,
            string outputFile
            )
        {
            const string ASYNC_SUFFIX = "Async";
            var          schemas      = new Dictionary <string, InvocationTargetDefinition>();

            // create a list of nested namespaces from the root namespace
            var namespaces = new List <string>();

            if (!string.IsNullOrEmpty(rootNamespace))
            {
                var parts = rootNamespace.Split(".");
                for (var i = 0; i < parts.Length; ++i)
                {
                    namespaces.Add(string.Join(".", parts.Take(i + 1)) + ".");
                }
            }
            namespaces.Add("");
            namespaces.Reverse();

            // enumerate type methods
            Console.WriteLine($"Inspecting method invocation targets in {directory}");
            foreach (var methodReference in methodReferences.Distinct())
            {
                InvocationTargetDefinition?entryPoint = null;
                try {
                    // extract class and method names from method reference
                    if (!StringEx.TryParseAssemblyClassMethodReference(methodReference, out var assemblyName, out var typeName, out var methodName))
                    {
                        throw new ProcessTargetInvocationException($"method reference '{methodReference}' is not well formed");
                    }

                    // load assembly
                    Assembly assembly;
                    var      assemblyFilepath = Path.Combine(directory, assemblyName + ".dll");
                    try {
                        assembly = Assembly.LoadFrom(assemblyFilepath);
                    } catch (FileNotFoundException) {
                        throw new ProcessTargetInvocationException($"could not find assembly '{assemblyFilepath}'");
                    } catch (Exception e) {
                        throw new ProcessTargetInvocationException($"error loading assembly '{assemblyFilepath}': {e.Message}");
                    }

                    // find type in assembly
                    var type = namespaces.Select(ns => assembly.GetType(ns + typeName)).Where(t => t != null).FirstOrDefault();
                    if (type == null)
                    {
                        throw new ProcessTargetInvocationException($"could not find type for '{methodReference}' in assembly '{assembly.FullName}'");
                    }

                    // find method, optionally with 'Async' suffix
                    var method = type.GetMethod(methodName);
                    if ((method == null) && !methodName.EndsWith(ASYNC_SUFFIX, StringComparison.Ordinal))
                    {
                        methodName += ASYNC_SUFFIX;
                        method      = type.GetMethod(methodName);
                    }
                    if (method == null)
                    {
                        throw new ProcessTargetInvocationException($"could not find method '{methodName}' in type '{type.FullName}'");
                    }
                    var resolvedMethodReference = $"{assemblyName}::{type.FullName}::{method.Name}";
                    var operationName           = methodName.EndsWith(ASYNC_SUFFIX, StringComparison.Ordinal)
                        ? methodName.Substring(0, methodName.Length - ASYNC_SUFFIX.Length)
                        : methodName;

                    // process method parameters
                    ParameterInfo?requestParameter      = null;
                    ParameterInfo?proxyRequestParameter = null;
                    var           uriParameters         = new List <KeyValuePair <string, bool> >();
                    var           parameters            = method.GetParameters();
                    foreach (var parameter in parameters)
                    {
                        // check if [FromUri] or [FromBody] attributes are present
                        var customAttributes     = parameter.GetCustomAttributes(true);
                        var hasFromUriAttribute  = customAttributes.Any(attribute => attribute.GetType().FullName == "LambdaSharp.ApiGateway.FromUriAttribute");
                        var hasFromBodyAttribute = customAttributes.Any(attribute => attribute.GetType().FullName == "LambdaSharp.ApiGateway.FromBodyAttribute");
                        if (hasFromUriAttribute && hasFromBodyAttribute)
                        {
                            throw new ProcessTargetInvocationException($"{resolvedMethodReference} parameter '{parameter.Name}' cannot have both [FromUri] and [FromBody] attributes");
                        }

                        // check if parameter is a proxy request
                        var isProxyRequest =
                            (parameter.ParameterType.FullName == "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyRequest") ||
                            (parameter.ParameterType.FullName == "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest");
                        if (isProxyRequest)
                        {
                            if (hasFromUriAttribute || hasFromBodyAttribute)
                            {
                                throw new ProcessTargetInvocationException($"{resolvedMethodReference} parameter '{parameter.Name}' of type 'APIGatewayProxyRequest' or 'APIGatewayHttpApiV2ProxyRequest' cannot have [FromUri] or [FromBody] attribute");
                            }
                            if (proxyRequestParameter != null)
                            {
                                throw new ProcessTargetInvocationException($"{resolvedMethodReference} parameters '{proxyRequestParameter.Name}' and '{parameter.Name}' conflict on proxy request");
                            }
                            proxyRequestParameter = parameter;
                            continue;
                        }

                        // check if parameter needs to deserialized from URI or BODY
                        var isSimpleType = parameter.ParameterType.IsValueType || (parameter.ParameterType == typeof(string));
                        if ((isSimpleType && !hasFromBodyAttribute) || hasFromUriAttribute)
                        {
                            // check if parameter is read from URI string directly or if its members are read from the URI string
                            if (isSimpleType)
                            {
                                // parameter is required only if it does not have an optional value and is not nullable
                                uriParameters.Add(new KeyValuePair <string, bool>(parameter.Name ?? throw new InvalidOperationException("missing parameter name"), !parameter.IsOptional && (Nullable.GetUnderlyingType(parameter.ParameterType) == null) && (parameter.ParameterType.IsValueType || parameter.ParameterType == typeof(string))));
                            }
                            else
                            {
                                var queryParameterType = parameter.ParameterType;

                                // add complex-type properties
                                foreach (var property in queryParameterType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
                                {
                                    var name =
                                        property.GetCustomAttribute <System.Text.Json.Serialization.JsonPropertyNameAttribute>()?.Name
                                        ?? property.GetCustomAttribute <System.Runtime.Serialization.DataMemberAttribute>()?.Name
                                        ?? property.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName
                                        ?? property.Name;
                                    var required = (
                                        (Nullable.GetUnderlyingType(property.PropertyType) == null) &&
                                        (property.PropertyType.IsValueType || (property.PropertyType == typeof(string))) &&
                                        (property.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required != Newtonsoft.Json.Required.Default) &&
                                        (property.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required != Newtonsoft.Json.Required.DisallowNull)
                                        ) ||
                                                   (property.GetCustomAttribute <System.ComponentModel.DataAnnotations.RequiredAttribute>() != null) ||
                                                   (property.GetCustomAttribute <Newtonsoft.Json.JsonRequiredAttribute>() != null) ||
                                                   (property.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required == Newtonsoft.Json.Required.Always) ||
                                                   (property.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required == Newtonsoft.Json.Required.AllowNull);
                                    uriParameters.Add(new KeyValuePair <string, bool>(name, required));
                                }

                                // NOTE (2020-08-10, bjorg): System.Text.Json does not deserialize fields

                                // add complex-type fields
                                foreach (var field in queryParameterType.GetFields(BindingFlags.Instance | BindingFlags.Public))
                                {
                                    var name     = field.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName ?? field.Name;
                                    var required = (
                                        (Nullable.GetUnderlyingType(field.FieldType) == null) &&
                                        (field.FieldType.IsValueType || (field.FieldType == typeof(string))) &&
                                        (field.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required != Newtonsoft.Json.Required.Default) &&
                                        (field.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required != Newtonsoft.Json.Required.DisallowNull)
                                        ) ||
                                                   (field.GetCustomAttribute <Newtonsoft.Json.JsonRequiredAttribute>() != null) ||
                                                   (field.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required == Newtonsoft.Json.Required.Always) ||
                                                   (field.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required == Newtonsoft.Json.Required.AllowNull);
                                    uriParameters.Add(new KeyValuePair <string, bool>(name, required));
                                }
                            }
                        }
                        else
                        {
                            if (requestParameter != null)
                            {
                                throw new ProcessTargetInvocationException($"{resolvedMethodReference} parameters '{requestParameter.Name}' and '{parameter.Name}' conflict on request body");
                            }
                            requestParameter = parameter;
                        }
                    }

                    // check if no specific request parameter was present, but the method also takes a proxy request
                    if ((requestParameter == null) && (proxyRequestParameter != null))
                    {
                        requestParameter = proxyRequestParameter;
                    }

                    // process method request type
                    var requestSchemaAndContentType = await AddSchema(methodReference, $"for parameter '{requestParameter?.Name}'", requestParameter?.ParameterType);

                    // process method response type
                    var responseType = (method.ReturnType.IsGenericType) && (method.ReturnType.GetGenericTypeDefinition() == typeof(Task <>))
                        ? method.ReturnType.GetGenericArguments()[0]
                        : method.ReturnType;
                    var responseSchemaAndContentType = await AddSchema(method.Name, "as return value", responseType);

                    entryPoint = new InvocationTargetDefinition {
                        Assembly            = assemblyName,
                        Type                = type.FullName,
                        Method              = methodName,
                        OperationName       = operationName,
                        RequestContentType  = requestSchemaAndContentType?.Item2,
                        RequestSchema       = requestSchemaAndContentType?.Item1,
                        RequestSchemaName   = requestParameter?.ParameterType.FullName,
                        UriParameters       = uriParameters.Any() ? new Dictionary <string, bool>(uriParameters) : null,
                        ResponseContentType = responseSchemaAndContentType?.Item2,
                        ResponseSchema      = responseSchemaAndContentType?.Item1,
                        ResponseSchemaName  = responseType?.FullName
                    };

                    // write result
                    Console.WriteLine($"... {resolvedMethodReference}({string.Join(", ", uriParameters.Select(kv => kv.Key))}) {entryPoint.GetRequestSchemaName()} -> {entryPoint.GetResponseSchemaName()}");
                } catch (ProcessTargetInvocationException e) {
                    entryPoint = new InvocationTargetDefinition {
                        Error = e.Message
                    };
                } catch (Exception e) {
                    entryPoint = new InvocationTargetDefinition {
                        Error      = $"internal error: {e.Message}",
                        StackTrace = e.StackTrace
                    };
                }
                if (entryPoint != null)
                {
                    schemas.Add(methodReference, entryPoint);
                }
                else
                {
                    schemas.Add(methodReference, new InvocationTargetDefinition {
                        Error = "internal error: missing target definition"
                    });
                }
            }

            // create json document
            try {
                var output = JsonSerializer.Serialize(schemas, new JsonSerializerOptions {
                    IgnoreNullValues = false,
                    WriteIndented    = true
                });
                if (outputFile != null)
                {
                    File.WriteAllText(outputFile, output);
                }
                else
                {
                    Console.WriteLine(output);
                }
            } catch (Exception e) {
                LogError("unable to write schema", e);
            }

            // local functions
            async Task <Tuple <object, string?> > AddSchema(string methodReference, string parameterName, Type?messageType)
            {
                // check if there is no request type
                if (messageType == null)
                {
                    return(Tuple.Create((object)"Void", (string?)null));
                }

                // check if there is no response type
                if (
                    (messageType == typeof(void)) ||
                    (messageType == typeof(Task))
                    )
                {
                    return(Tuple.Create((object)"Void", (string?)null));
                }

                // check if request/response type is not supported
                if (
                    (messageType == typeof(string)) ||
                    messageType.IsValueType
                    )
                {
                    throw new ProcessTargetInvocationException($"{methodReference} has unsupported type {parameterName}");
                }

                // check if request/response type is inside 'Task<T>'
                if (messageType.IsGenericType && messageType.GetGenericTypeDefinition() == typeof(Task <>))
                {
                    messageType = messageType.GetGenericArguments()[0];
                }

                // check if request/response has an open-ended schema
                if (
                    (messageType == typeof(object)) ||
                    (messageType == typeof(Newtonsoft.Json.Linq.JObject))
                    )
                {
                    return(Tuple.Create((object)"Object", (string?)"application/json"));
                }

                // check if request/response is not a proxy request/response
                if (
                    (messageType.FullName != "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyRequest") &&
                    (messageType.FullName != "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse") &&
                    (messageType.FullName != "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest") &&
                    (messageType.FullName != "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse")
                    )
                {
                    var schema = await JsonSchema4.FromTypeAsync(messageType, new JsonSchemaGeneratorSettings {
                        FlattenInheritanceHierarchy = true,

#pragma warning disable CS0618
                        // we prefer enums to be handled as strings (NOTE: trying to set this in SerializerSettings causes an NRE in JsonSchema4FromTypeAsync call)
                        DefaultEnumHandling = EnumHandling.String
#pragma warning restore CS0618
                    });

                    // NOTE (2019-04-03, bjorg): we need to allow additional properties, because Swagger doesn't support: "additionalProperties": false
                    schema.AllowAdditionalProperties = true;
                    foreach (var definition in schema.Definitions)
                    {
                        definition.Value.AllowAdditionalProperties = true;
                    }

                    // NOTE (2019-08-16, bjorg): don't emit "x-enumNames" as it is not supported by API Gateway
                    foreach (var definition in schema.Definitions)
                    {
                        definition.Value.EnumerationNames = null;
                    }

                    // return JSON schema document
                    return(Tuple.Create((object)(JsonToNativeConverter.ParseObject(schema.ToJson()) ?? throw new InvalidDataException("schema is not a valid JSON object")), (string?)"application/json"));
                }
                return(Tuple.Create((object)"Proxy", (string?)null));
            }
        }
コード例 #3
0
ファイル: AppBuilder.cs プロジェクト: bgkyer/LambdaSharpTool
        //--- Methods ---
        public void Build(
            IApp app,
            bool noCompile,
            bool noAssemblyValidation,
            string?gitSha,
            string?gitBranch,
            string buildConfiguration,
            bool forceBuild,
            out string platform,
            out string framework,
            out string appVersionId
            )
        {
            // read settings from project file
            var projectFile     = new CSharpProjectFile(app.Project);
            var targetFramework = projectFile.TargetFramework;

            // set output parameters
            platform  = "Blazor WebAssembly";
            framework = targetFramework;

            // check if any app files are newer than the most recently built package; otherwise, skip build
            var appMetadataFilepath = Path.Combine(Provider.OutputDirectory, $"appmetadata_{Provider.ModuleFullName}_{app.LogicalId}.json");

            if (!forceBuild)
            {
                var appPackage = Provider.ExistingPackages.FirstOrDefault(p =>
                                                                          Path.GetFileName(p).StartsWith($"app_{Provider.ModuleFullName}_{app.LogicalId}_", StringComparison.Ordinal) &&
                                                                          p.EndsWith(".zip", StringComparison.Ordinal)
                                                                          );
                if ((appPackage != null) && File.Exists(appMetadataFilepath))
                {
                    LogInfoVerbose($"=> Analyzing app {app.FullName} dependencies");

                    // find all files used to create the app package
                    var files = new HashSet <string>();
                    CSharpProjectFile.DiscoverDependencies(
                        files,
                        app.Project,
                        filePath => LogInfoVerbose($"... analyzing {filePath}"),
                        (message, exception) => LogError(message, exception)
                        );

                    // check if any of the files has been modified more recently than the app package
                    var appPackageDate = File.GetLastWriteTime(appPackage);
                    var file           = files.FirstOrDefault(f => File.GetLastWriteTime(f) > appPackageDate);
                    if (file == null)
                    {
                        try {
                            // attempt to load extract assembly metadata
                            var appMetadata = JsonSerializer.Deserialize <LambdaSharpTool.AssemblyMetadata>(File.ReadAllText(appMetadataFilepath));
                            if (appMetadata.ModuleVersionId != null)
                            {
                                Provider.WriteLine($"=> Skipping app {Provider.InfoColor}{app.FullName}{Provider.ResetColor} (no changes found)");

                                // keep the existing files
                                Provider.ExistingPackages.Remove(appMetadataFilepath);
                                Provider.ExistingPackages.Remove(appPackage);

                                // set the module variable to the final package name
                                appVersionId = appMetadata.ModuleVersionId;
                                Provider.AddArtifact($"{app.FullName}::PackageName", appPackage);
                                return;
                            }
                        } catch {
                            // ignore exception and continue
                        }
                    }
                    else
                    {
                        LogInfoVerbose($"... found newer file: {file}");
                    }
                }
            }
            else
            {
                LogInfoVerbose($"=> Analyzing app {app.FullName} dependencies");

                // find all files used to create the app package
                var files = new HashSet <string>();
                CSharpProjectFile.DiscoverDependencies(
                    files,
                    app.Project,
                    filePath => LogInfoVerbose($"... analyzing {filePath}"),
                    (message, exception) => LogError(message, exception)
                    );

                // loop over all project folders
                new CleanBuildFolders(BuildEventsConfig).Do(files);
            }

            // validate the project is using the most recent lambdasharp assembly references
            if (
                !noAssemblyValidation &&
                app.HasAssemblyValidation &&
                !projectFile.ValidateLambdaSharpPackageReferences(Provider.ToolVersion, LogWarn, LogError)
                )
            {
                appVersionId = "<MISSING>";
                return;
            }
            if (noCompile)
            {
                appVersionId = "<MISSING>";
                return;
            }

            // compile app project
            Provider.WriteLine($"=> Building app {Provider.InfoColor}{app.FullName}{Provider.ResetColor} [{projectFile.TargetFramework}, {buildConfiguration}]");
            var projectDirectory = Path.Combine(Provider.WorkingDirectory, Path.GetFileNameWithoutExtension(app.Project));

            if (File.Exists(appMetadataFilepath))
            {
                File.Delete(appMetadataFilepath);
            }

            // build and publish Blazor app
            if (!DotNetBuildBlazor(projectFile.TargetFramework, buildConfiguration, projectDirectory))
            {
                // nothing to do; error was already reported
                appVersionId = "<MISSING>";
                return;
            }

            // extract version id from app
            var assemblyFilepath = (string.Compare(targetFramework, "netstandard2.1", StringComparison.Ordinal) == 0)
                ? Path.Combine(projectDirectory, "bin", buildConfiguration, targetFramework, "publish", "wwwroot", "_framework", "_bin", $"{Path.GetFileNameWithoutExtension(app.Project)}.dll")
                : Path.Combine(projectDirectory, "bin", buildConfiguration, targetFramework, "publish", "wwwroot", "_framework", $"{Path.GetFileNameWithoutExtension(app.Project)}.dll");
            var assemblyMetadata = LambdaSharpAppAssemblyInformation(assemblyFilepath, appMetadataFilepath);

            if (assemblyMetadata?.ModuleVersionId == null)
            {
                LogError($"unable to extract assembly metadata");
                appVersionId = "<MISSING>";
                return;
            }
            Provider.ExistingPackages.Remove(appMetadataFilepath);
            LogInfoVerbose($"... assembly version id: {assemblyMetadata.ModuleVersionId}");
            appVersionId = assemblyMetadata.ModuleVersionId;

            // update `blazor.boot.json` file by adding `appsettings.Production.json` to config list
            var wwwRootFolder = Path.Combine(projectDirectory, "bin", buildConfiguration, targetFramework, "publish", "wwwroot");

            if (File.Exists(Path.Combine(wwwRootFolder, AppSettingsProductionJsonFileName)))
            {
                LogError($"'{AppSettingsProductionJsonFileName}' is reserved for loading deployment generated configuration settings and cannot be used explicitly");
                return;
            }

            var blazorBootJsonFileName = Path.Combine(wwwRootFolder, "_framework", "blazor.boot.json");
            var blazorBootJson         = JsonToNativeConverter.ParseObject(File.ReadAllText(blazorBootJsonFileName));

            if (
                (blazorBootJson != null) &&
                (blazorBootJson.TryGetValue("config", out var blazorBootJsonConfig)) &&
                (blazorBootJsonConfig is List <object?> blazorBootJsonConfigList)
                )
            {
                var blazorBootJsonModified = false;
                if (!blazorBootJsonConfigList.Contains(AppSettingsJsonFileName) && ((gitSha != null) || (gitBranch != null)))
                {
                    // add instruction to load appsettings.json
                    blazorBootJsonConfigList.Add(AppSettingsJsonFileName);
                    blazorBootJsonModified = true;
                }
                if (!blazorBootJsonConfigList.Contains(AppSettingsProductionJsonFileName))
                {
                    // add instruction to load appsettings.Production.json
                    blazorBootJsonConfigList.Add(AppSettingsProductionJsonFileName);
                    blazorBootJsonModified = true;
                }
                if (blazorBootJsonModified)
                {
                    LogInfoVerbose("... updating 'blazor.boot.json' configuration file");
                    File.WriteAllText(blazorBootJsonFileName, JsonSerializer.Serialize(blazorBootJson));
                }
            }
            else
            {
                LogError($"unable to update {blazorBootJsonFileName}");
            }

            // zip output folder
            BuildPackage(app, gitSha, gitBranch, wwwRootFolder);
        }