Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion TypeContractor.Tool/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public Generator(string assemblyPath,
_casing = casing;
}

public Task<int> Execute()
public Task<int> Execute(CancellationToken cancellationToken)
{
var returnCode = 0;

Expand Down
187 changes: 127 additions & 60 deletions TypeContractor.Tool/Program.cs
Original file line number Diff line number Diff line change
@@ -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<string>("--assembly", "Path to the assembly to start with. Will be relative to the current directory");
var outputOption = new Option<string>("--output", "Output path to write to. Will be relative to the current directory");
var relativeRootOption = new Option<string>("--root", "Relative root for generating cleaner imports. For example '~/api'");
var cleanOption = new Option<CleanMethod>("--clean", () => CleanMethod.Smart, "Choose how to clean up no longer relevant type files in output directory. Danger!");
var replaceOptions = new Option<string[]>("--replace", "Provide one replacement in the form '<search>:<replace>'. Can be repeated");
var stripOptions = new Option<string[]>("--strip", "Provide a prefix to strip out of types. Can be repeated");
var mapOptions = new Option<string[]>("--custom-map", "Provide a custom type map in the form '<from>:<to>'. Can be repeated");
var packsOptions = new Option<string>("--packs-path", () => @"C:\Program Files\dotnet\packs\", "Path where dotnet is installed and reference assemblies can be found.");
var dotnetVersionOptions = new Option<int>("--dotnet-version", () => 8, "Major version of dotnet to look for");
var logLevelOptions = new Option<LogLevel>("--log-level", () => LogLevel.Info);
var buildZodSchemasOptions = new Option<bool>("--build-zod-schemas", () => false, "Enable experimental support for Zod schemas alongside generated types.");
var generateApiClientsOptions = new Option<bool>("--generate-api-clients", () => false, "Enable experimental support for auto-generating API clients for each endpoint.");
var apiClientsTemplateOptions = new Option<string>("--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", () => 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<string>("--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<string>("--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<string>("--root")
{
DefaultValueFactory = (arg) => config.TryGetString("root") ?? "",
Description = "Relative root for generating cleaner imports. For example '~/api'",
};

var cleanOption = new Option<CleanMethod>("--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<string[]>("--replace")
{
DefaultValueFactory = (arg) => config.GetStrings("replace"),
Description = "Provide one replacement in the form '<search>:<replace>'. Can be repeated",
};

var stripOptions = new Option<string[]>("--strip")
{
DefaultValueFactory = (arg) => config.GetStrings("strip"),
Description = "Provide a prefix to strip out of types. Can be repeated",
};

var mapOptions = new Option<string[]>("--custom-map")
{
DefaultValueFactory = (arg) => config.GetStrings("custom-map"),
Description = "Provide a custom type map in the form '<from>:<to>'. Can be repeated",
};

var packsOptions = new Option<string>("--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<int>("--dotnet-version")
{
DefaultValueFactory = (arg) => config.GetNumberWithFallback("dotnet-version", 8),
Description = "Major version of dotnet to look for",
};

var logLevelOptions = new Option<LogLevel>("--log-level")
{
DefaultValueFactory = (arg) => config.GetEnum("log-level", LogLevel.Info),
};

var buildZodSchemasOptions = new Option<bool>("--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<bool>("--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<string>("--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>("--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,
Expand All @@ -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();
4 changes: 2 additions & 2 deletions TypeContractor.Tool/TypeContractor.Tool.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand Down Expand Up @@ -26,7 +26,7 @@

<ItemGroup>
<PackageReference Include="DotNetConfig" Version="1.2.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0-rc.1.25451.107" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading