RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.69k stars 1.23k forks source link

Question: Optional namespace with MSBuild TypeScript generation #4465

Open AndreyTalanin opened 1 year ago

AndreyTalanin commented 1 year ago

Hi, I'm trying to generate a TypeScript client without a namespace via MSBuild's OpenApiReference.

First, If I specify a custom namespace, it uses it in the generated client, that's working fine:

  <ItemGroup>
    <OpenApiReference
      Include="Schema.json"
      CodeGenerator="NSwagTypeScript"
      ClassName="ApiClient"
      Namespace="My.Custom.Namespace"
      OutputPath="ApiClient.ts"
    >
    </OpenApiReference>
  </ItemGroup>

Then I try to opt out of generated namespace, but it starts to use the root project's namespace instead (here I changed Namespace=""):

  <ItemGroup>
    <OpenApiReference
      Include="Schema.json"
      CodeGenerator="NSwagTypeScript"
      ClassName="ApiClient"
      Namespace=""
      OutputPath="ApiClient.ts"
    >
    </OpenApiReference>
  </ItemGroup>

Not specifying the Namespace property at all causes the exact same behavior.

Is it possible to opt out of namespace generation when using MSBuild task?

Thank you.

dankay-hah commented 3 weeks ago

Having the same issue. Is there no way to exclude the namespace using this method to generate it?

AndreyTalanin commented 3 weeks ago

@dankay-hah, so far I came up with the following solution:

  1. Generate a client somewhere in the obj folder.
  2. During the build, copy the file from obj to sources.
  3. Run a String.Replace-powered find-and-replace to export the generated namespace in the generated client source file.
  4. Create an extra TypeScript source file to re-export types from the exported namespace.

In terms of code:

<PropertyGroup>
  <SpaRoot>..\MusicLibrarySuite.Application.ReactSpa\</SpaRoot>
  <!-- Path to the destination source file in your codebase. -->
  <ApplicationClientFileName>$(SpaRoot)source\api\ApplicationClient.ts</ApplicationClientFileName>
  <!-- Path to the temporary client source file, generated automatically by NSwag. -->
  <IntermediateApplicationClientFileName>$(BaseIntermediateOutputPath)TypeScript\ApplicationClient.ts</IntermediateApplicationClientFileName>
</PropertyGroup>
<ItemGroup>
  <OpenApiReference
    Include="OpenApi\Specifications\MusicLibrarySuite.Application.json"
    CodeGenerator="NSwagTypeScript"
    Namespace="MusicLibrarySuite.Application.Client"
    ClassName="ApplicationClient"
    OutputPath="TypeScript\ApplicationClient.ts">
  </OpenApiReference>
</ItemGroup>
<Target Name="DebugFormatApplicationClient" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(ApplicationClientFileName)') ">
  <!-- Copy the TypeScript client to the output folder if it does not exist. -->
  <Copy SourceFiles="$(IntermediateApplicationClientFileName)" DestinationFiles="$(ApplicationClientFileName)" />
  <!-- Export the generated TypeScript namespace with String.Replace. -->
  <WriteLinesToFile File="$(ApplicationClientFileName)" Lines="$([System.IO.File]::ReadAllText($(ApplicationClientFileName)).Replace('namespace MusicLibrarySuite.Application.Client', 'export namespace MusicLibrarySuite.Application.Client'))" Overwrite="true" />
  <!-- Format the generated 'ApplicationClient.ts' file. -->
  <Exec WorkingDirectory="$(SpaRoot)" Command="npx prettier --write $(ApplicationClientFileName)" />
</Target>

Please, note, that with the Condition=" '$(Configuration)' == 'Debug' And !Exists('$(ApplicationClientFileName)') " condition this MSBuild target will be skipped unless you delete the client manually. You can make use of that behavior or you can remove the condition.

What you will get as result is a TypeScript client that looks like this:

//----------------------
// <auto-generated>
//     Generated using the NSwag toolchain v13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------

/* tslint:disable */
/* eslint-disable */
// ReSharper disable InconsistentNaming

export namespace MusicLibrarySuite.Application.Client {
  export class ApplicationClient {
    // code here
  }
}

Then I created an index.ts in the same folder:

import { MusicLibrarySuite } from "./ApplicationClient";

export class ApplicationClient extends MusicLibrarySuite.Application.Client.ApplicationClient {}
export class ApiException extends MusicLibrarySuite.Application.Client.ApiException {}

Here I had to create my own classes and extend namespace-prefixed ones. From this point I can use my exported classes without namespaces:

import { ApplicationClient } from "./api";

const applicationClient: ApplicationClient = new ApplicationClient();

applicationClient
  .checkReadiness()
  .then(() => alert("Yeeeeey, we have a working backend!"))
  .catch(() => alert("Today's not so lucky..."));

My solution's probably far from being ideal, but it does almost everything I need and is somewhat automated. You need to delete the old client file to trigger copying of the new one and you may need to create some extra types for export forwarding like I did with ApplicationClient and ApiException, but otherwise it works reliably.

Hope you find this useful.