Skip to content
Closed
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `line-endings` option (`--line-endings`) to choose between `crlf` (default) and `lf` in generated files

### Fixed

- Generated files no longer mix CRLF and LF line endings on macOS and Linux

## [0.22.1] - 2026-08-23

### Fixed
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,13 @@ dotnet-config tool for viewing and changing configuration similar to how
root = "~/api"
generate-api-clients = true
build-zod-schemas = true
line-endings = "lf" # Defaults to "crlf"
```

Generated files always use the same line endings, regardless of the OS
TypeContractor runs on. The default is `crlf`; set `line-endings = "lf"` (or
pass `--line-endings lf`) to use Unix line endings instead.

## Run manually

Get an instance of `Contractor` and call `contractor.Build();`
Expand Down
39 changes: 39 additions & 0 deletions TypeContractor.Tests/TypeScript/ApiClientWriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,45 @@ public void Can_Write_Basic_Client()
.And.Contain("return await response.json();");
}

[Fact]
public void Defaults_To_Crlf_Line_Endings()
{
// Arrange
var apiClient = new ApiClient("TestClient", "TestController", "test", null);
apiClient.AddEndpoint(new ApiClientEndpoint("getLatestId", "latest", EndpointMethod.GET, typeof(Guid), typeof(Guid), false, [], null));

// Act
var result = Sut.Write(apiClient, [], _converter, false, _templateFn, Casing.Pascal);

// Assert
var text = File.ReadAllText(result);
text.Should().Contain("\r\n");
text.Replace("\r\n", "", StringComparison.Ordinal).Should()
.NotContain("\r")
.And.NotContain("\n");
}

[Theory]
[InlineData(LineEndings.Crlf, "\r\n")]
[InlineData(LineEndings.Lf, "\n")]
public void Uses_Configured_Line_Endings(LineEndings lineEndings, string newLine)
{
// Arrange
var apiClient = new ApiClient("TestClient", "TestController", "test", null);
apiClient.AddEndpoint(new ApiClientEndpoint("getLatestId", "latest", EndpointMethod.GET, typeof(Guid), typeof(Guid), false, [], null));
var sut = new ApiClientWriter(_configuration.OutputPath, "~", lineEndings);

// Act
var result = sut.Write(apiClient, [], _converter, false, _templateFn, Casing.Pascal);

// Assert
var text = File.ReadAllText(result);
text.Should().Contain(newLine);
text.Replace(newLine, "", StringComparison.Ordinal).Should()
.NotContain("\r")
.And.NotContain("\n");
}

[Fact]
public void Strips_Trailing_Slash()
{
Expand Down
37 changes: 37 additions & 0 deletions TypeContractor.Tests/TypeScript/TypeScriptWriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,43 @@ public void Can_Write_Simple_Types()
.And.Contain("someObject: any;");
}

[Fact]
public void Defaults_To_Crlf_Line_Endings()
{
// Arrange
var outputTypes = BuildOutputTypes(typeof(SimpleTypes));

// Act
var result = Sut.Write(outputTypes.First(), outputTypes, true);

// Assert
var text = File.ReadAllText(result);
text.Should().Contain("\r\n");
text.Replace("\r\n", "", StringComparison.Ordinal).Should()
.NotContain("\r")
.And.NotContain("\n");
}

[Theory]
[InlineData(LineEndings.Crlf, "\r\n")]
[InlineData(LineEndings.Lf, "\n")]
public void Uses_Configured_Line_Endings(LineEndings lineEndings, string newLine)
{
// Arrange
var outputTypes = BuildOutputTypes(typeof(SimpleTypes));
var sut = new TypeScriptWriter(_configuration.OutputPath, lineEndings);

// Act
var result = sut.Write(outputTypes.First(), outputTypes, true);

// Assert
var text = File.ReadAllText(result);
text.Should().Contain(newLine);
text.Replace(newLine, "", StringComparison.Ordinal).Should()
.NotContain("\r")
.And.NotContain("\n");
}

[Fact]
public void Can_Write_Generic_Types()
{
Expand Down
8 changes: 7 additions & 1 deletion TypeContractor.Tool/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal class Generator
private readonly bool _generateApiClients;
private readonly string _apiClientTemplate;
private readonly Casing? _casing;
private readonly LineEndings? _lineEndings;

public Generator(string assemblyPath,
string[] alsoLookIn,
Expand All @@ -38,7 +39,8 @@ public Generator(string assemblyPath,
bool buildZodSchemas,
bool generateApiClients,
string apiClientTemplate,
Casing? casing)
Casing? casing,
LineEndings? lineEndings)
{
_assemblyPath = assemblyPath;
_alsoLookIn = alsoLookIn;
Expand All @@ -55,6 +57,7 @@ public Generator(string assemblyPath,
_generateApiClients = generateApiClients;
_apiClientTemplate = apiClientTemplate;
_casing = casing;
_lineEndings = lineEndings;
}

public Task<int> Execute(CancellationToken cancellationToken)
Expand Down Expand Up @@ -239,6 +242,9 @@ private Contractor GenerateContractor(Dictionary<Assembly, HashSet<Type>> typesT
if (_casing is not null)
configuration = configuration.SetCasing(_casing.Value);

if (_lineEndings is not null)
configuration = configuration.SetLineEndings(_lineEndings.Value);

return Contractor.WithConfiguration(configuration);
}
}
11 changes: 10 additions & 1 deletion TypeContractor.Tool/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@
Description = "Casing to use for generated file names",
};

var lineEndingsOptions = new Option<LineEndings>("--line-endings")
{
DefaultValueFactory = (arg) => config.GetEnum("line-endings", LineEndings.Crlf),
Description = "Line endings to use in generated files. Either 'crlf' (default) or 'lf'",
};

rootCommand.Options.Add(assemblyOption);
rootCommand.Options.Add(extraAssemblyOptions);
rootCommand.Options.Add(outputOption);
Expand All @@ -122,6 +128,7 @@
rootCommand.Options.Add(generateApiClientsOptions);
rootCommand.Options.Add(apiClientsTemplateOptions);
rootCommand.Options.Add(casingOptions);
rootCommand.Options.Add(lineEndingsOptions);

apiClientsTemplateOptions.Validators.Add(result =>
{
Expand Down Expand Up @@ -161,6 +168,7 @@
var generateApiClientsValue = parseResult.GetValue(generateApiClientsOptions);
var apiClientsTemplateValue = parseResult.GetValue(apiClientsTemplateOptions)!;
var casingValue = parseResult.GetValue(casingOptions);
var lineEndingsValue = parseResult.GetValue(lineEndingsOptions);

Log.Instance = new ConsoleLogger(logLevelValue);
var generator = new Generator(assemblyOptionValue,
Expand All @@ -177,7 +185,8 @@
buildZodSchemasValue,
generateApiClientsValue,
apiClientsTemplateValue,
casingValue);
casingValue,
lineEndingsValue);

return await generator.Execute(cancellationToken);
});
Expand Down
4 changes: 2 additions & 2 deletions TypeContractor/Contractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public int Build(MetadataLoadContext? metadataLoadContext = null, bool smartClea
var allTypes = new List<OutputType>();

var converter = new TypeScriptConverter(Configuration, metadataLoadContext);
var writer = new TypeScriptWriter(Configuration.OutputPath);
var writer = new TypeScriptWriter(Configuration.OutputPath, Configuration.LineEndings);

foreach (var (assemblyName, assemblyPath) in Configuration.Assemblies)
{
Expand Down Expand Up @@ -134,7 +134,7 @@ public int Build(MetadataLoadContext? metadataLoadContext = null, bool smartClea
}

var templateFn = Handlebars.Compile(template);
var apiWriter = new ApiClientWriter(Configuration.OutputPath, Configuration.RelativeRoot);
var apiWriter = new ApiClientWriter(Configuration.OutputPath, Configuration.RelativeRoot, Configuration.LineEndings);
foreach (var client in Configuration.ApiClients)
{
try
Expand Down
23 changes: 23 additions & 0 deletions TypeContractor/Helpers/LineEndingHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Text.RegularExpressions;

namespace TypeContractor.Helpers;

internal static partial class LineEndingHelpers
{
[GeneratedRegex(@"\r\n|\r|\n")]
private static partial Regex LineBreakRegex();

public static string ToNewLine(this LineEndings lineEndings) => lineEndings switch
{
LineEndings.Crlf => "\r\n",
LineEndings.Lf => "\n",
_ => throw new ArgumentOutOfRangeException(nameof(lineEndings), lineEndings, "Unknown line ending"),
};

/// <summary>
/// Replaces every line break in <paramref name="input"/> with the configured line ending,
/// so generated files are identical regardless of the OS TypeContractor runs on.
/// </summary>
public static string NormalizeLineEndings(string input, LineEndings lineEndings) =>
LineBreakRegex().Replace(input, lineEndings.ToNewLine());
}
14 changes: 14 additions & 0 deletions TypeContractor/LineEndings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace TypeContractor;

public enum LineEndings
{
/// <summary>
/// Windows line endings (\r\n)
/// </summary>
Crlf,

/// <summary>
/// Unix line endings (\n)
/// </summary>
Lf,
}
12 changes: 12 additions & 0 deletions TypeContractor/TypeContractorConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class TypeContractorConfiguration
private List<ApiClient> _apiClients = [];
private string? _apiClientTemplate;
private Casing? _casing;
private LineEndings? _lineEndings;

public IReadOnlyDictionary<string, string> TypeMaps => _map;
public IReadOnlyList<string> Suffixes => _suffixes.AsReadOnly();
Expand All @@ -31,6 +32,7 @@ public class TypeContractorConfiguration
public IReadOnlyList<ApiClient> ApiClients => _apiClients.AsReadOnly();
public string ApiClientTemplate => _apiClientTemplate ?? "aurelia";
public Casing Casing => _casing ?? Casing.Kebab;
public LineEndings LineEndings => _lineEndings ?? LineEndings.Crlf;

/// <summary>
/// Set up a default configuration using <see cref="AddDefaultSuffixes"/> and <see cref="AddDefaultTypeMaps"/>
Expand Down Expand Up @@ -292,6 +294,16 @@ public TypeContractorConfiguration SetCasing(Casing casing)
return this;
}

/// <summary>
/// Set the line endings to use in generated files. Default is <c>crlf</c>.
/// </summary>
/// <returns>The configuration object for continued chaining</returns>
public TypeContractorConfiguration SetLineEndings(LineEndings lineEndings)
{
_lineEndings = lineEndings;
return this;
}

/// <summary>
/// Enable generation of API clients for relevant endpoints.
///
Expand Down
4 changes: 2 additions & 2 deletions TypeContractor/TypeScript/ApiClientWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace TypeContractor.TypeScript;

public partial class ApiClientWriter(string outputPath, string? relativeRoot)
public partial class ApiClientWriter(string outputPath, string? relativeRoot, LineEndings lineEndings = LineEndings.Crlf)
{
private static readonly Encoding _utf8WithoutBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private static readonly Dictionary<EndpointMethod, string> _httpMethods = new()
Expand Down Expand Up @@ -165,7 +165,7 @@ public string Write(ApiClient apiClient, IEnumerable<OutputType> allTypes, TypeS
Directory.CreateDirectory(directory);

// Write file
File.WriteAllText(filePath, result.Trim() + Environment.NewLine, _utf8WithoutBom);
File.WriteAllText(filePath, LineEndingHelpers.NormalizeLineEndings(result.Trim(), lineEndings) + lineEndings.ToNewLine(), _utf8WithoutBom);

// Return the path we wrote to
return filePath;
Expand Down
4 changes: 2 additions & 2 deletions TypeContractor/TypeScript/TypeScriptWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
namespace TypeContractor.TypeScript;

#pragma warning disable CA1305 // Specify IFormatProvider
public class TypeScriptWriter(string outputPath)
public class TypeScriptWriter(string outputPath, LineEndings lineEndings = LineEndings.Crlf)
{
private static readonly Encoding _utf8WithoutBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private readonly StringBuilder _builder = new();
Expand All @@ -31,7 +31,7 @@ public string Write(OutputType outputType, IEnumerable<OutputType> allTypes, boo
Directory.CreateDirectory(directory);

// Write file
File.WriteAllText(filePath, _builder.ToString().Trim() + Environment.NewLine, _utf8WithoutBom);
File.WriteAllText(filePath, LineEndingHelpers.NormalizeLineEndings(_builder.ToString().Trim(), lineEndings) + lineEndings.ToNewLine(), _utf8WithoutBom);

// Return the path we wrote to
return filePath;
Expand Down