diff --git a/CHANGELOG.md b/CHANGELOG.md index cbe1db9..2750f8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Generate Zod v4 schemas (#168) - Use `globalThis.Response` instead of `Response` to avoid conflicts (#147) - Set kebab-case as the default casing (#166) +- Update System.CommandLine to latest release candidate (#163) ## [0.17.4.1] - 2025-09-19 diff --git a/TypeContractor.Tool/Generator.cs b/TypeContractor.Tool/Generator.cs index 586deca..b8b68b3 100644 --- a/TypeContractor.Tool/Generator.cs +++ b/TypeContractor.Tool/Generator.cs @@ -51,7 +51,7 @@ public Generator(string assemblyPath, _casing = casing; } - public Task Execute() + public Task Execute(CancellationToken cancellationToken) { var returnCode = 0; diff --git a/TypeContractor.Tool/Program.cs b/TypeContractor.Tool/Program.cs index 7535040..76a5694 100644 --- a/TypeContractor.Tool/Program.cs +++ b/TypeContractor.Tool/Program.cs @@ -1,84 +1,150 @@ using DotNetConfig; using System.CommandLine; -using System.CommandLine.Parsing; using TypeContractor; using TypeContractor.Logger; using TypeContractor.Tool; +using TypeContractor.Tool.vendor; var config = Config.Build("typecontractor.config"); var rootCommand = new RootCommand("Tool for generating TypeScript definitions from C# code"); -var assemblyOption = new Option("--assembly", "Path to the assembly to start with. Will be relative to the current directory"); -var outputOption = new Option("--output", "Output path to write to. Will be relative to the current directory"); -var relativeRootOption = new Option("--root", "Relative root for generating cleaner imports. For example '~/api'"); -var cleanOption = new Option("--clean", () => CleanMethod.Smart, "Choose how to clean up no longer relevant type files in output directory. Danger!"); -var replaceOptions = new Option("--replace", "Provide one replacement in the form ':'. Can be repeated"); -var stripOptions = new Option("--strip", "Provide a prefix to strip out of types. Can be repeated"); -var mapOptions = new Option("--custom-map", "Provide a custom type map in the form ':'. Can be repeated"); -var packsOptions = new Option("--packs-path", () => @"C:\Program Files\dotnet\packs\", "Path where dotnet is installed and reference assemblies can be found."); -var dotnetVersionOptions = new Option("--dotnet-version", () => 8, "Major version of dotnet to look for"); -var logLevelOptions = new Option("--log-level", () => LogLevel.Info); -var buildZodSchemasOptions = new Option("--build-zod-schemas", () => false, "Enable experimental support for Zod schemas alongside generated types."); -var generateApiClientsOptions = new Option("--generate-api-clients", () => false, "Enable experimental support for auto-generating API clients for each endpoint."); -var apiClientsTemplateOptions = new Option("--api-client-template", () => "aurelia", "Template to use for API clients. Either 'aurelia', 'react-axios' (built-in) or a path to a Handlebars file, including extension"); -var casingOptions = new Option("--casing", () => Casing.Kebab, "Casing to use for generated file names"); -assemblyOption.IsRequired = true; -outputOption.IsRequired = true; - -rootCommand.AddOption(assemblyOption); -rootCommand.AddOption(outputOption); -rootCommand.AddOption(relativeRootOption); -rootCommand.AddOption(cleanOption); -rootCommand.AddOption(replaceOptions); -rootCommand.AddOption(stripOptions); -rootCommand.AddOption(mapOptions); -rootCommand.AddOption(packsOptions); -rootCommand.AddOption(dotnetVersionOptions); -rootCommand.AddOption(logLevelOptions); -rootCommand.AddOption(buildZodSchemasOptions); -rootCommand.AddOption(generateApiClientsOptions); -rootCommand.AddOption(apiClientsTemplateOptions); -rootCommand.AddOption(casingOptions); - -apiClientsTemplateOptions.AddValidator(result => -{ - var value = result.GetValueForOption(apiClientsTemplateOptions)!; + +var assemblyOption = new Option("--assembly") +{ + DefaultValueFactory = (arg) => config.TryGetString("assembly") ?? "", + Description = "Path to the assembly to start with. Will be relative to the current directory", + Required = true, +}; + +var outputOption = new Option("--output") +{ + DefaultValueFactory = (arg) => config.TryGetString("output") ?? "", + Description = "Output path to write to. Will be relative to the current directory", + Required = true, +}; + +var relativeRootOption = new Option("--root") +{ + DefaultValueFactory = (arg) => config.TryGetString("root") ?? "", + Description = "Relative root for generating cleaner imports. For example '~/api'", +}; + +var cleanOption = new Option("--clean") +{ + DefaultValueFactory = (arg) => config.GetEnum("clean", CleanMethod.Smart), + Description = "Choose how to clean up no longer relevant type files in output directory. Danger!", +}; + +var replaceOptions = new Option("--replace") +{ + DefaultValueFactory = (arg) => config.GetStrings("replace"), + Description = "Provide one replacement in the form ':'. Can be repeated", +}; + +var stripOptions = new Option("--strip") +{ + DefaultValueFactory = (arg) => config.GetStrings("strip"), + Description = "Provide a prefix to strip out of types. Can be repeated", +}; + +var mapOptions = new Option("--custom-map") +{ + DefaultValueFactory = (arg) => config.GetStrings("custom-map"), + Description = "Provide a custom type map in the form ':'. Can be repeated", +}; + +var packsOptions = new Option("--packs-path") +{ + DefaultValueFactory = (arg) => config.GetStringWithFallback("packs-path", @"C:\Program Files\dotnet\packs\"), + Description = "Path where dotnet is installed and reference assemblies can be found.", +}; + +var dotnetVersionOptions = new Option("--dotnet-version") +{ + DefaultValueFactory = (arg) => config.GetNumberWithFallback("dotnet-version", 8), + Description = "Major version of dotnet to look for", +}; + +var logLevelOptions = new Option("--log-level") +{ + DefaultValueFactory = (arg) => config.GetEnum("log-level", LogLevel.Info), +}; + +var buildZodSchemasOptions = new Option("--build-zod-schemas") +{ + DefaultValueFactory = (arg) => config.GetBoolean("build-zod-schemas", false), + Description = "Enable experimental support for Zod schemas alongside generated types.", +}; + +var generateApiClientsOptions = new Option("--generate-api-clients") +{ + DefaultValueFactory = (arg) => config.GetBoolean("generate-api-clients", false), + Description = "Enable experimental support for auto-generating API clients for each endpoint.", +}; + +var apiClientsTemplateOptions = new Option("--api-client-template") +{ + DefaultValueFactory = (arg) => config.GetStringWithFallback("api-client-template", "aurelia"), + Description = "Template to use for API clients. Either 'aurelia', 'react-axios' (built-in) or a path to a Handlebars file, including extension", +}; + +var casingOptions = new Option("--casing") +{ + DefaultValueFactory = (arg) => config.GetEnum("casing", Casing.Kebab), + Description = "Casing to use for generated file names", +}; + +rootCommand.Options.Add(assemblyOption); +rootCommand.Options.Add(outputOption); +rootCommand.Options.Add(relativeRootOption); +rootCommand.Options.Add(cleanOption); +rootCommand.Options.Add(replaceOptions); +rootCommand.Options.Add(stripOptions); +rootCommand.Options.Add(mapOptions); +rootCommand.Options.Add(packsOptions); +rootCommand.Options.Add(dotnetVersionOptions); +rootCommand.Options.Add(logLevelOptions); +rootCommand.Options.Add(buildZodSchemasOptions); +rootCommand.Options.Add(generateApiClientsOptions); +rootCommand.Options.Add(apiClientsTemplateOptions); +rootCommand.Options.Add(casingOptions); + +apiClientsTemplateOptions.Validators.Add(result => +{ + var value = result.GetValue(apiClientsTemplateOptions)!; if (value.Equals("aurelia", StringComparison.CurrentCultureIgnoreCase) || value.Equals("react-axios", StringComparison.CurrentCultureIgnoreCase)) return; - var generateClients = result.GetValueForOption(generateApiClientsOptions); + var generateClients = result.GetValue(generateApiClientsOptions); if (!generateClients) { - result.ErrorMessage = $"Must generate API clients for --{apiClientsTemplateOptions.Name} to have any effect."; + result.AddError($"Must generate API clients for --{apiClientsTemplateOptions.Name} to have any effect."); return; } if (!File.Exists(value)) { - result.ErrorMessage = $"The template specified does not exist or is not readable. Searched for {Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), value))}."; + result.AddError($"The template specified does not exist or is not readable. Searched for {Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), value))}."); return; } }); -// Apply configuration from file, if any -rootCommand = rootCommand.WithConfigurableDefaults("typecontractor", config); - -rootCommand.SetHandler(async (context) => -{ - var assemblyOptionValue = context.ParseResult.GetValueForOption(assemblyOption)!; - var outputValue = context.ParseResult.GetValueForOption(outputOption)!; - var relativeRootValue = context.ParseResult.GetValueForOption(relativeRootOption); - var cleanValue = context.ParseResult.GetValueForOption(cleanOption); - var replacementsValue = context.ParseResult.GetValueForOption(replaceOptions) ?? []; - var stripValue = context.ParseResult.GetValueForOption(stripOptions) ?? []; - var customMapsValue = context.ParseResult.GetValueForOption(mapOptions) ?? []; - var packsPathValue = context.ParseResult.GetValueForOption(packsOptions)!; - var dotnetVersionValue = context.ParseResult.GetValueForOption(dotnetVersionOptions); - var logLevelValue = context.ParseResult.GetValueForOption(logLevelOptions); - var buildZodSchemasValue = context.ParseResult.GetValueForOption(buildZodSchemasOptions); - var generateApiClientsValue = context.ParseResult.GetValueForOption(generateApiClientsOptions); - var apiClientsTemplateValue = context.ParseResult.GetValueForOption(apiClientsTemplateOptions)!; - var casingValue = context.ParseResult.GetValueForOption(casingOptions); +rootCommand.SetAction(async (parseResult, cancellationToken) => +{ + var assemblyOptionValue = parseResult.GetValue(assemblyOption)!; + var outputValue = parseResult.GetValue(outputOption)!; + var relativeRootValue = parseResult.GetValue(relativeRootOption); + var cleanValue = parseResult.GetValue(cleanOption); + var replacementsValue = parseResult.GetValue(replaceOptions) ?? []; + var stripValue = parseResult.GetValue(stripOptions) ?? []; + var customMapsValue = parseResult.GetValue(mapOptions) ?? []; + var packsPathValue = parseResult.GetValue(packsOptions)!; + var dotnetVersionValue = parseResult.GetValue(dotnetVersionOptions); + var logLevelValue = parseResult.GetValue(logLevelOptions); + var buildZodSchemasValue = parseResult.GetValue(buildZodSchemasOptions); + var generateApiClientsValue = parseResult.GetValue(generateApiClientsOptions); + var apiClientsTemplateValue = parseResult.GetValue(apiClientsTemplateOptions)!; + var casingValue = parseResult.GetValue(casingOptions); Log.Instance = new ConsoleLogger(logLevelValue); var generator = new Generator(assemblyOptionValue, @@ -95,7 +161,8 @@ apiClientsTemplateValue, casingValue); - context.ExitCode = await generator.Execute(); + return await generator.Execute(cancellationToken); }); -return await rootCommand.InvokeAsync(args); +var parsedResult = rootCommand.Parse(args); +return await parsedResult.InvokeAsync(); diff --git a/TypeContractor.Tool/TypeContractor.Tool.csproj b/TypeContractor.Tool/TypeContractor.Tool.csproj index fbb7883..32fb2da 100644 --- a/TypeContractor.Tool/TypeContractor.Tool.csproj +++ b/TypeContractor.Tool/TypeContractor.Tool.csproj @@ -1,4 +1,4 @@ - + Exe @@ -26,7 +26,7 @@ - + diff --git a/TypeContractor.Tool/vendor/CommandLineExtensions.cs b/TypeContractor.Tool/vendor/CommandLineExtensions.cs deleted file mode 100644 index be8f797..0000000 --- a/TypeContractor.Tool/vendor/CommandLineExtensions.cs +++ /dev/null @@ -1,392 +0,0 @@ -using DotNetConfig; -using HandlebarsDotNet; -using System.Collections.Concurrent; - -namespace System.CommandLine -{ - /// - /// Extension methods to automatically read default values for arguments and - /// options from .netconfig. - /// - /// - /// After invoking on a command, all its - /// arguments and options in the entire command tree are processed according to - /// these heuristics: - /// - /// - /// - /// Only arguments/options without a default value are processed - /// - /// - /// - /// - /// Section matches root command name, subsection (dot - separated) for each additional nested - /// command level(i.e. `[mytool "mycommand.myverb"]`) - /// - /// - /// - /// Compatible arguments/options(same name/type) can be placed in ancestor section/subsection to affect - /// default value of entire subtree - /// - /// - /// - /// All the types supported by System.CommandLine for multiple artity arguments and options are - /// automatically populated: arrays, `IEnumerable{T}`, `ICollection{T}`, `IList{T}` and `List{T}`: - /// .netconfig can provide multi-valued variables for those - /// - /// - /// - /// - /// Numbers can be either `int` or `long`. - /// - /// - /// - /// - public static class CommandLineExtensions - { - static readonly HashSet _configurableTypes = new( - [ - typeof(string), - typeof(long), - typeof(long?), - typeof(int), - typeof(int?), - typeof(bool), - typeof(bool?), - typeof(DateTime), - typeof(DateTime?), - ]); - - /// - /// Register default value factories for all arguments and options in the command tree - /// which don't have a default value already. - /// - /// Type of command, inferred from usage. - /// The command to set default values for. - /// Optional root section for this command arguments and its tree. If not provided, the 's Name - /// property will be used. For a , this is the app's assembly name. - /// Optional pre-built configuration. If not provided, will - /// be used to build a default configuration. - /// The command where defaults have been applied. - public static T WithConfigurableDefaults(this T command, string? section = default, Config? configuration = default) where T : Command - { - configuration ??= Config.Build(); - section ??= command.Name; - - var arguments = new HashSet(); - CollectArguments(command, arguments); - - var sections = new ConcurrentDictionary<(string? subsection, string variable), HashSet>(); - PopulateSections(command, sections, arguments, null); - - // Clear sections with multiple variables but with incompatible types - foreach (var entry in sections.Where(x => x.Value.GroupBy(arg => arg.ValueType).Skip(1).Any()).ToArray()) - sections.TryRemove(entry.Key, out _); - - // Remaining sections are all the final arguments for defaulting. - var factories = new ConcurrentDictionary(); - foreach (var sharedSection in sections) - { - foreach (var argument in sharedSection.Value) - { - factories.GetOrAdd(argument, arg => new DefaultValueFactory(arg, configuration, section)) - .AddSubsection(sharedSection.Key.subsection); - } - } - - foreach (var factory in factories.Values) - factory.SetDefaultValue(); - - return command; - } - - static void PopulateSections(Command command, - ConcurrentDictionary<(string? subsection, string variable), HashSet> sections, - HashSet arguments, - string? subsection = null) - { - void Add(IEnumerable args) - { - foreach (var arg in args) - { - if (subsection != null) - { - var parts = subsection.Split('.'); - for (var i = 1; i <= parts.Length; i++) - sections.GetOrAdd((string.Join(".", parts[0..i]), arg.Name), _ => []).Add(arg); - - sections.GetOrAdd((null, arg.Name), _ => []).Add(arg); - } - else - { - sections.GetOrAdd((subsection, arg.Name), _ => []).Add(arg); - } - } - } - - Add(command.Arguments.Where(arguments.Contains)); - Add(command.Options.OfType