diff --git a/src/StrawberryShake/MetaPackages/Blazor/MSBuild/StrawberryShake.Blazor.props b/src/StrawberryShake/MetaPackages/Blazor/MSBuild/StrawberryShake.Blazor.props index 79162641d54..ae8c28b5ec1 100644 --- a/src/StrawberryShake/MetaPackages/Blazor/MSBuild/StrawberryShake.Blazor.props +++ b/src/StrawberryShake/MetaPackages/Blazor/MSBuild/StrawberryShake.Blazor.props @@ -10,6 +10,11 @@ md5 intermediate + + + + default + disable diff --git a/src/StrawberryShake/MetaPackages/Common/MSBuild/StrawberryShake.targets b/src/StrawberryShake/MetaPackages/Common/MSBuild/StrawberryShake.targets index 0660063b0c4..190f40581b3 100644 --- a/src/StrawberryShake/MetaPackages/Common/MSBuild/StrawberryShake.targets +++ b/src/StrawberryShake/MetaPackages/Common/MSBuild/StrawberryShake.targets @@ -22,16 +22,19 @@ Condition="'@(GraphQL)' != ''"> $(MSBuildProjectDirectory)\$(IntermediateOutputPath)berry\ + $(MSBuildProjectDirectory)\$(GraphQLPersistedQueryFormat) dotnet $(GenTool) generate "$(MSBuildProjectDirectory)" $(GenCommand) -o "$(GraphQLCodeGenerationRoot)" $(GenCommand) -n "$(RootNamespace)" + $(GenCommand) -q "$(GraphQLQueryGenerationRoot)" $(GenCommand) -a "$(GraphQLRequestHash)" $(GenCommand) -s $(GenCommand) -t $(GenCommand) -r + $(GenCommand) --relayFormat diff --git a/src/StrawberryShake/MetaPackages/Maui/MSBuild/StrawberryShake.Maui.props b/src/StrawberryShake/MetaPackages/Maui/MSBuild/StrawberryShake.Maui.props index 5932725030c..131d5c3c86e 100644 --- a/src/StrawberryShake/MetaPackages/Maui/MSBuild/StrawberryShake.Maui.props +++ b/src/StrawberryShake/MetaPackages/Maui/MSBuild/StrawberryShake.Maui.props @@ -10,6 +10,11 @@ md5 intermediate + + + + default + disable diff --git a/src/StrawberryShake/MetaPackages/Server/MSBuild/StrawberryShake.Server.props b/src/StrawberryShake/MetaPackages/Server/MSBuild/StrawberryShake.Server.props index 7958f055eb1..5f2c69f2d7b 100644 --- a/src/StrawberryShake/MetaPackages/Server/MSBuild/StrawberryShake.Server.props +++ b/src/StrawberryShake/MetaPackages/Server/MSBuild/StrawberryShake.Server.props @@ -10,6 +10,11 @@ md5 intermediate + + + + default + disable diff --git a/src/StrawberryShake/Tooling/.vscode/launch.json b/src/StrawberryShake/Tooling/.vscode/launch.json index 267277223e0..767264f96ce 100644 --- a/src/StrawberryShake/Tooling/.vscode/launch.json +++ b/src/StrawberryShake/Tooling/.vscode/launch.json @@ -13,11 +13,13 @@ "generate", "/Users/michael/local/play/StrawberryBuildTests", "-o /Users/michael/local/play/StrawberryBuildTests/obj/Debug/net7.0/berry/", + "-q /Users/michael/local/play/StrawberryBuildTests/obj/Debug/net7.0/berry/q", "-n StrawberryBuildTests", "-a md5", "-s", "-t", - "-r" + "-r", + "--relayFormat" ], "cwd": "${workspaceFolder}/src/dotnet-graphql", "console": "internalConsole", diff --git a/src/StrawberryShake/Tooling/src/dotnet-graphql/ExportCommand.cs b/src/StrawberryShake/Tooling/src/dotnet-graphql/ExportCommand.cs deleted file mode 100644 index e24361e2ebd..00000000000 --- a/src/StrawberryShake/Tooling/src/dotnet-graphql/ExportCommand.cs +++ /dev/null @@ -1,121 +0,0 @@ -using McMaster.Extensions.CommandLineUtils; -using StrawberryShake.CodeGeneration.CSharp; -using StrawberryShake.Tools.Configuration; -using static System.Environment; -using static StrawberryShake.Tools.GeneratorHelpers; - -namespace StrawberryShake.Tools; - -public static class ExportCommand -{ - public static void Build(CommandLineApplication generate) - { - generate.Description = "Exports Persisted Queries for Strawberry Shake Clients"; - - var pathArg = generate.Argument( - "path", - "The project directory."); - - var razorArg = generate.Option( - "-o|--outputPath", - "Output Directory.", - CommandOptionType.SingleValue); - - var relayFormatArg = generate.Option( - "-r|--relayFormat", - "Export Persisted Queries as Relay Format.", - CommandOptionType.NoValue); - - var jsonArg = generate.Option( - "-j|--json", - "Console output as JSON.", - CommandOptionType.NoValue); - - generate.OnExecuteAsync(ct => - { - var arguments = new ExportCommandArguments( - pathArg.Value ?? CurrentDirectory, - razorArg.Value()!, - relayFormatArg.HasValue()); - var handler = CommandTools.CreateHandler(jsonArg); - return handler.ExecuteAsync(arguments, ct); - }); - } - - private sealed class ExportCommandHandler : CommandHandler - { - public ExportCommandHandler(IConsoleOutput output) - { - Output = output; - } - - public IConsoleOutput Output { get; } - - public override async Task ExecuteAsync( - ExportCommandArguments arguments, - CancellationToken cancellationToken) - { - /* - using var activity = Output.WriteActivity("Export Persisted Queries"); - - if (string.IsNullOrEmpty(arguments.OutputPath)) - { - activity.WriteError(new HotChocolate.Error( - "The Output Directory `-o` must be set!")); - } - - var generator = new CSharpGeneratorClient(GetCodeGenServerLocation()); - var documents = GetDocuments(arguments.Path); - var configFiles = GetConfigFiles(arguments.Path); - - foreach (var configFileName in configFiles) - { - var config = await LoadConfigAsync(configFileName); - - var persistedDir = configFiles.Length == 1 - ? arguments.OutputPath - : Path.Combine(arguments.OutputPath, config.Extensions.StrawberryShake.Name); - - var request = new GeneratorRequest( - configFileName, - documents, - persistedQueryDirectory: persistedDir, - option: arguments.RelayFormat - ? RequestOptions.ExportPersistedQueriesJson - : RequestOptions.ExportPersistedQueries); - - var response = generator.Execute(request); - - if (response.TryLogErrors(activity)) - { - return 1; - } - } - */ - - return 0; - } - - private static async Task LoadConfigAsync(string configFileName) - { - var json = await File.ReadAllTextAsync(configFileName); - return GraphQLConfig.FromJson(json); - } - } - - private sealed class ExportCommandArguments - { - public ExportCommandArguments(string path, string outputPath, bool relayFormat) - { - Path = path; - OutputPath = outputPath; - RelayFormat = relayFormat; - } - - public string Path { get; } - - public string OutputPath { get; } - - public bool RelayFormat { get; } - } -} diff --git a/src/StrawberryShake/Tooling/src/dotnet-graphql/GenerateCommand.cs b/src/StrawberryShake/Tooling/src/dotnet-graphql/GenerateCommand.cs index 86de3d81baa..52d63dcae6a 100644 --- a/src/StrawberryShake/Tooling/src/dotnet-graphql/GenerateCommand.cs +++ b/src/StrawberryShake/Tooling/src/dotnet-graphql/GenerateCommand.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using McMaster.Extensions.CommandLineUtils; using StrawberryShake.CodeGeneration.CSharp; using StrawberryShake.Tools.Configuration; @@ -46,11 +47,29 @@ public static void Build(CommandLineApplication generate) "The output directory.", CommandOptionType.SingleValue); + var queryOutputDirArg = generate.Option( + "-q|--queryOutputDirectory", + "The output directory for persisted query files.", + CommandOptionType.SingleValue); + + var relayFormatArg = generate.Option( + "--relayFormat", + "Export persisted queries in the relay format.", + CommandOptionType.NoValue); + var jsonArg = generate.Option( "-j|--json", "Console output as JSON.", CommandOptionType.NoValue); + var strategy = RequestStrategy.Default; + var queryOutputDir = queryOutputDirArg.Value(); + + if (!string.IsNullOrEmpty(queryOutputDir) || relayFormatArg.HasValue()) + { + strategy = RequestStrategy.PersistedQuery; + } + generate.OnExecuteAsync(ct => { var arguments = new GenerateCommandArguments( @@ -61,7 +80,10 @@ public static void Build(CommandLineApplication generate) true, disableStoreArg.HasValue(), razorComponentsArg.HasValue(), - outputDirArg.Value()); + outputDirArg.Value(), + strategy, + queryOutputDir, + relayFormatArg.HasValue()); var handler = CommandTools.CreateHandler(jsonArg); return handler.ExecuteAsync(arguments, ct); }); @@ -91,10 +113,14 @@ public GenerateCommandHandler(IConsoleOutput output) var rootNamespace = args.RootNamespace ?? $"{clientName}NS"; var documents = GetGraphQLDocuments(args.Path, config.Documents); var settings = CreateSettings(config, args, rootNamespace); - var result = CSharpGenerator.Generate(documents, settings); + var result = GenerateClient(settings.ClientName, documents, settings); var outputDir = args.OutputDir ?? Path.Combine( Path.GetDirectoryName(configFileName)!, config.Extensions.StrawberryShake.OutputDirectoryName); + var queryOutputDir = args.QueryOutputDir ?? Path.Combine( + Path.GetDirectoryName(configFileName)!, + config.Extensions.StrawberryShake.OutputDirectoryName, + "Queries"); if (result.HasErrors()) { @@ -103,30 +129,15 @@ public GenerateCommandHandler(IConsoleOutput output) } else { - foreach (var doc in result.Documents) + await WriteCodeFilesAsync(result, outputDir, cancellationToken); + + if (args.Strategy is RequestStrategy.PersistedQuery) { - if (doc.Kind is SourceDocumentKind.CSharp or SourceDocumentKind.Razor) - { - var fileName = CreateFileName(outputDir, doc.Path, doc.Name, doc.Kind); - var dir = Path.GetDirectoryName(fileName)!; - - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - - if (File.Exists(fileName)) - { - File.Delete(fileName); - } - - await File.WriteAllTextAsync( - fileName, - doc.SourceText, - cancellationToken); - - Output.WriteFileCreated(fileName); - } + await WritePersistedQueriesAsync( + result, + queryOutputDir, + args.RelayFormat, + cancellationToken); } } } @@ -134,7 +145,79 @@ public GenerateCommandHandler(IConsoleOutput output) return statusCode; } - private static string CreateFileName( + private CSharpGeneratorResult GenerateClient( + string clientName, + string[] documents, + CSharpGeneratorSettings settings) + { + using var activity = Output.WriteActivity($"Generate {clientName}"); + return CSharpGenerator.Generate(documents, settings); + } + + private async Task WriteCodeFilesAsync( + CSharpGeneratorResult result, + string outputDir, + CancellationToken cancellationToken) + { + foreach (var doc in result.Documents) + { + if (doc.Kind is SourceDocumentKind.CSharp or SourceDocumentKind.Razor) + { + var fileName = CreateCodeFileName(outputDir, doc.Path, doc.Name, doc.Kind); + + EnsureWeCanWriteTheFile(fileName); + + await File.WriteAllTextAsync( + fileName, + doc.SourceText, + cancellationToken); + + Output.WriteFileCreated(fileName); + } + } + } + + private static async Task WritePersistedQueriesAsync( + CSharpGeneratorResult result, + string outputDir, + bool relayFormat, + CancellationToken cancellationToken) + { + if (relayFormat) + { + var map = new Dictionary(); + + foreach (var doc in result.Documents) + { + map[doc.Hash!] = doc.SourceText; + } + + var fileName = Path.Combine(outputDir, "queries.json"); + + EnsureWeCanWriteTheFile(fileName); + + await File.WriteAllTextAsync( + fileName, + JsonSerializer.Serialize(map), + cancellationToken); + } + else + { + foreach (var doc in result.Documents) + { + var fileName = Path.Combine(outputDir, $"{doc.Hash}.graphql"); + + EnsureWeCanWriteTheFile(fileName); + + await File.WriteAllTextAsync( + fileName, + doc.SourceText, + cancellationToken); + } + } + } + + private static string CreateCodeFileName( string outputDir, string? path, string name, @@ -149,6 +232,21 @@ public GenerateCommandHandler(IConsoleOutput output) ? Path.Combine(outputDir, $"{name}.{kindName}.cs") : Path.Combine(outputDir, path, $"{name}.{kindName}.cs"); } + + private static void EnsureWeCanWriteTheFile(string fileName) + { + var dir = Path.GetDirectoryName(fileName)!; + + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + } } internal sealed class GenerateCommandArguments @@ -161,7 +259,10 @@ internal sealed class GenerateCommandArguments bool useSingleFile, bool noStore, bool razorComponents, - string? outputDir) + string? outputDir, + RequestStrategy strategy, + string? queryOutputDir, + bool relayFormat) { Path = path; RootNamespace = rootNamespace; @@ -171,6 +272,9 @@ internal sealed class GenerateCommandArguments NoStore = noStore; RazorComponents = razorComponents; OutputDir = outputDir; + Strategy = strategy; + QueryOutputDir = queryOutputDir ?? outputDir; + RelayFormat = relayFormat; } public string Path { get; } @@ -188,5 +292,11 @@ internal sealed class GenerateCommandArguments public bool RazorComponents { get; } public string? OutputDir { get; } + + public RequestStrategy Strategy { get; } + + public string? QueryOutputDir { get; } + + public bool RelayFormat { get; } } } diff --git a/src/StrawberryShake/Tooling/src/dotnet-graphql/Program.cs b/src/StrawberryShake/Tooling/src/dotnet-graphql/Program.cs index c91620f3cbf..df43c402156 100644 --- a/src/StrawberryShake/Tooling/src/dotnet-graphql/Program.cs +++ b/src/StrawberryShake/Tooling/src/dotnet-graphql/Program.cs @@ -15,7 +15,6 @@ internal static Task Main(string[] args) root.Command("update", UpdateCommand.Build); root.Command("download", DownloadCommand.Build); root.Command("generate", GenerateCommand.Build); - root.Command("export", ExportCommand.Build); root.Command("where", WhereCommand.Build); root.OnExecute(() => diff --git a/src/StrawberryShake/Tooling/src/dotnet-graphql/UpdateCommandHandler.cs b/src/StrawberryShake/Tooling/src/dotnet-graphql/UpdateCommandHandler.cs index c6d757a7b85..c62121d4137 100644 --- a/src/StrawberryShake/Tooling/src/dotnet-graphql/UpdateCommandHandler.cs +++ b/src/StrawberryShake/Tooling/src/dotnet-graphql/UpdateCommandHandler.cs @@ -1,16 +1,9 @@ -using System; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; -using System.Threading; -using StrawberryShake.Tools.OAuth; using System.Text; using StrawberryShake.Tools.Configuration; namespace StrawberryShake.Tools { - public class UpdateCommandHandler - : CommandHandler + public class UpdateCommandHandler : CommandHandler { public UpdateCommandHandler( IFileSystem fileSystem, @@ -107,17 +100,61 @@ await UpdateSchemaAsync(context, clientDirectory, configuration, cancellationTok { var uri = new Uri(configuration.Extensions.StrawberryShake.Url); var schemaFilePath = Path.Combine(clientDirectory, configuration.Schema); + var tempFile = CreateTempFileName(); - if (!await DownloadSchemaAsync(context, uri, schemaFilePath, cancellationToken) + // we first attempt to download the new schema into a temp file. + // if that should fail we still have the original schema file and + // the user can still work. + if (!await DownloadSchemaAsync(context, uri, tempFile, cancellationToken) .ConfigureAwait(false)) { + // if the schema download succeeded we will replace the old schema with the + // new one. + if (File.Exists(schemaFilePath)) + { + File.Delete(schemaFilePath); + } + + File.Move(tempFile, schemaFilePath); + hasErrors = true; } + + // in any case we will make sure the temp file is removed at the end. + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } } return !hasErrors; } + private static string CreateTempFileName() + { + var pathSegment = Random.Shared.Next(9999).ToString(); + string tempFile; + + for (var i = 0; i < 100; i++) + { + tempFile = Path.Combine(Path.GetTempPath(), pathSegment, Path.GetRandomFileName()); + + if (!File.Exists(tempFile)) + { + var tempDir = Path.GetDirectoryName(tempFile)!; + + if (!Directory.Exists(tempDir)) + { + Directory.CreateDirectory(tempDir); + } + + return tempFile; + } + } + + throw new InvalidOperationException("Could not acquire a temp file."); + } + private async Task DownloadSchemaAsync( UpdateCommandContext context, Uri serviceUri,