Skip to content

Commit e149aab

Browse files
Add 'plugins' emitter option to load generator plugins from paths (#10249)
## Problem Fixes #8196 Defining custom visitors currently requires creating a separate npm package (with package.json) and a custom �mitter-package.json to reference it. This is heavy ceremony for what should be a simple task. ## Solution Add a \plugins\ emitter option to \@typespec/http-client-csharp\ that accepts an array of paths to plugin assemblies (DLLs) or directories. The generator loads plugins from these paths using the existing MEF composition pipeline. ### Usage \\\yaml options: "@typespec/http-client-csharp": plugins: - path/to/MyPlugin.dll - path/to/another-plugin/dist \\\ Each plugin must contain a class extending \GeneratorPlugin\. ### What this eliminates - npm \package.json\ wrapper for plugin DLLs - Custom \�mitter-package.json\ with plugin dependency - Custom \�mitterPackageJsonPath\ in \ sp-location.yaml\ ### Changes **TypeScript emitter:** - \options.ts\ — Added \plugins\ to \CSharpEmitterOptions\ interface and JSON schema (\string[]\) - \�mitter.ts\ — Resolves relative paths to absolute before writing to Configuration.json **C# generator:** - \Configuration.cs\ — Added \PluginPaths\ as a top-level property (\IReadOnlyList<string>?\) - \GeneratorHandler.cs\ — Added \AddConfiguredPluginDlls()\ that loads plugin assemblies into the MEF \AggregateCatalog\ before composition, so they participate in the same discovery pipeline as node_modules plugins --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 03b8f4c commit e149aab

14 files changed

Lines changed: 984 additions & 8 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
title: "Decorators"
3+
description: "Decorators exported by @typespec/http-client-csharp"
4+
toc_min_heading_level: 2
5+
toc_max_heading_level: 3
6+
---
7+
8+
## TypeSpec.HttpClient.CSharp
9+
10+
### `@dynamicModel` {#@TypeSpec.HttpClient.CSharp.dynamicModel}
11+
12+
Marks a model or namespace as dynamic, indicating it should generate dynamic model code.
13+
Can be applied to Model or Namespace types.
14+
15+
```typespec
16+
@TypeSpec.HttpClient.CSharp.dynamicModel
17+
```
18+
19+
#### Target
20+
21+
`Model | Namespace`
22+
23+
#### Parameters
24+
25+
None
26+
27+
#### Examples
28+
29+
```tsp
30+
@dynamicModel
31+
model Pet {
32+
name: string;
33+
kind: string;
34+
}
35+
36+
@dynamicModel
37+
namespace PetStore {
38+
model Dog extends Pet {
39+
breed: string;
40+
}
41+
}
42+
```
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
---
2+
title: "Emitter usage"
3+
---
4+
5+
## Emitter usage
6+
7+
1. Via the command line
8+
9+
```bash
10+
tsp compile . --emit=@typespec/http-client-csharp
11+
```
12+
13+
2. Via the config
14+
15+
```yaml
16+
emit:
17+
- "@typespec/http-client-csharp"
18+
```
19+
20+
The config can be extended with options as follows:
21+
22+
```yaml
23+
emit:
24+
- "@typespec/http-client-csharp"
25+
options:
26+
"@typespec/http-client-csharp":
27+
option: value
28+
```
29+
30+
## Emitter options
31+
32+
### `emitter-output-dir`
33+
34+
**Type:** `absolutePath`
35+
36+
Defines the emitter output directory. Defaults to `{output-dir}/@typespec/http-client-csharp`
37+
See [Configuring output directory for more info](https://typespec.io/docs/handbook/configuration/configuration/#configuring-output-directory)
38+
39+
### `api-version`
40+
41+
**Type:** `string`
42+
43+
For TypeSpec files using the [`@versioned`](https://typespec.io/docs/libraries/versioning/reference/decorators/#@TypeSpec.Versioning.versioned) decorator, set this option to the version that should be used to generate against.
44+
45+
### `generate-protocol-methods`
46+
47+
**Type:** `boolean`
48+
49+
Set to `false` to skip generation of protocol methods. The default value is `true`.
50+
51+
### `generate-convenience-methods`
52+
53+
**Type:** `boolean`
54+
55+
Set to `false` to skip generation of convenience methods. The default value is `true`.
56+
57+
### `unreferenced-types-handling`
58+
59+
**Type:** `"removeOrInternalize" | "internalize" | "keepAll"`
60+
61+
Defines the strategy on how to handle unreferenced types. The default value is `removeOrInternalize`.
62+
63+
### `new-project`
64+
65+
**Type:** `boolean`
66+
67+
Set to `true` to overwrite the csproj if it already exists. The default value is `false`.
68+
69+
### `save-inputs`
70+
71+
**Type:** `boolean`
72+
73+
Set to `true` to save the `tspCodeModel.json` and `Configuration.json` files that are emitted and used as inputs to the generator. The default value is `false`.
74+
75+
### `package-name`
76+
77+
**Type:** `string`
78+
79+
Define the package name. If not specified, the first namespace defined in the TypeSpec is used as the package name.
80+
81+
### `debug`
82+
83+
**Type:** `boolean`
84+
85+
Set to `true` to automatically attempt to attach to a debugger when executing the C# generator. The default value is `false`.
86+
87+
### `logLevel`
88+
89+
**Type:** `"info" | "debug" | "verbose"`
90+
91+
Set the log level for which to collect traces. The default value is `info`.
92+
93+
### `disable-xml-docs`
94+
95+
**Type:** `boolean`
96+
97+
Set to `true` to disable XML documentation generation. The default value is `false`.
98+
99+
### `generator-name`
100+
101+
**Type:** `string`
102+
103+
The name of the generator. By default this is set to `ScmCodeModelGenerator`. Generator authors can set this to the name of a generator that inherits from `ScmCodeModelGenerator`.
104+
105+
### `emitter-extension-path`
106+
107+
**Type:** `string`
108+
109+
Allows emitter authors to specify the path to a custom emitter package, allowing you to extend the emitter behavior. This should be set to `import.meta.url` if you are using a custom emitter.
110+
111+
### `plugins`
112+
113+
**Type:** `array`
114+
115+
Paths to generator plugin assemblies (DLLs) or directories containing plugin assemblies. Each plugin must contain a class that extends GeneratorPlugin.
116+
117+
### `license`
118+
119+
**Type:** `object`
120+
121+
License information for the generated client code.
122+
123+
### `sdk-context-options`
124+
125+
**Type:** `object`
126+
127+
The SDK context options that implement the `CreateSdkContextOptions` interface from the [`@azure-tools/typespec-client-generator-core`](https://www.npmjs.com/package/@azure-tools/typespec-client-generator-core) package to be used by the CSharp emitter.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
title: Overview
3+
sidebar_position: 0
4+
toc_min_heading_level: 2
5+
toc_max_heading_level: 3
6+
---
7+
8+
import { Tabs, TabItem } from "@astrojs/starlight/components";
9+
10+
TypeSpec library for emitting Http Client libraries for C#.
11+
12+
## Install
13+
14+
<Tabs>
15+
<TabItem label="In a spec" default>
16+
17+
```bash
18+
npm install @typespec/http-client-csharp
19+
```
20+
21+
</TabItem>
22+
<TabItem label="In a library" default>
23+
24+
```bash
25+
npm install --save-peer @typespec/http-client-csharp
26+
```
27+
28+
</TabItem>
29+
</Tabs>
30+
31+
## Emitter usage
32+
33+
[See documentation](./emitter.md)
34+
35+
## TypeSpec.HttpClient
36+
37+
## TypeSpec.HttpClient.CSharp
38+
39+
### Decorators
40+
41+
- [`@dynamicModel`](./decorators.md#@TypeSpec.HttpClient.CSharp.dynamicModel)

packages/http-client-csharp/emitter/src/emitter.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
resolvePath,
1414
} from "@typespec/compiler";
1515
import fs, { statSync } from "fs";
16-
import { dirname } from "path";
16+
import { dirname, resolve } from "path";
1717
import { fileURLToPath } from "url";
1818
import { writeCodeModel, writeConfiguration } from "./code-model-writer.js";
1919
import {
@@ -84,6 +84,11 @@ export async function emitCodeModel(
8484
const options = resolveOptions(context);
8585
const outputFolder = context.emitterOutputDir;
8686

87+
// Resolve plugin paths to absolute if specified
88+
if (options["plugins"]) {
89+
options["plugins"] = options["plugins"].map((p) => resolve(outputFolder, p));
90+
}
91+
8792
/* set the log level. */
8893
const logger = new Logger(program, options.logLevel ?? LoggerLevel.INFO);
8994

packages/http-client-csharp/emitter/src/options.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface CSharpEmitterOptions {
1818
"disable-xml-docs"?: boolean;
1919
"generator-name"?: string;
2020
"emitter-extension-path"?: string;
21+
plugins?: string[];
2122
"sdk-context-options"?: CreateSdkContextOptions;
2223
"generate-protocol-methods"?: boolean;
2324
"generate-convenience-methods"?: boolean;
@@ -113,6 +114,14 @@ export const CSharpEmitterOptionsSchema: JSONSchemaType<CSharpEmitterOptions> =
113114
description:
114115
"Allows emitter authors to specify the path to a custom emitter package, allowing you to extend the emitter behavior. This should be set to `import.meta.url` if you are using a custom emitter.",
115116
},
117+
plugins: {
118+
type: "array",
119+
items: { type: "string" },
120+
nullable: true,
121+
description:
122+
"Paths to generator plugin assemblies (DLLs) or directories containing plugin assemblies. " +
123+
"Each plugin must contain a class that extends GeneratorPlugin.",
124+
},
116125
license: {
117126
type: "object",
118127
additionalProperties: false,

packages/http-client-csharp/emitter/test/Unit/options.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,27 @@ describe("Configuration tests", async () => {
162162
expect(config["generate-protocol-methods"]).toBeUndefined();
163163
expect(config["generate-convenience-methods"]).toBeUndefined();
164164
});
165+
166+
it("should pass plugins option to configuration", async () => {
167+
const options: CSharpEmitterOptions = {
168+
"package-name": "test-package",
169+
plugins: ["/path/to/Plugin.dll", "/path/to/plugin-dir"],
170+
};
171+
const context = createEmitterContext(program, options);
172+
const sdkContext = await createCSharpSdkContext(context);
173+
const config = createConfiguration(options, "namespace", sdkContext);
174+
175+
expect(config["plugins"]).toEqual(["/path/to/Plugin.dll", "/path/to/plugin-dir"]);
176+
});
177+
178+
it("should not include plugins in configuration when not set", async () => {
179+
const options: CSharpEmitterOptions = {
180+
"package-name": "test-package",
181+
};
182+
const context = createEmitterContext(program, options);
183+
const sdkContext = await createCSharpSdkContext(context);
184+
const config = createConfiguration(options, "namespace", sdkContext);
185+
186+
expect(config["plugins"]).toBeUndefined();
187+
});
165188
});

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,16 @@ public Configuration(
3535
string packageName,
3636
bool disableXmlDocs,
3737
UnreferencedTypesHandlingOption unreferencedTypesHandling,
38-
LicenseInfo? licenseInfo)
38+
LicenseInfo? licenseInfo,
39+
IReadOnlyList<string>? pluginPaths = null)
3940
{
4041
OutputDirectory = outputPath;
4142
AdditionalConfigurationOptions = additionalConfigurationOptions;
4243
PackageName = packageName;
4344
DisableXmlDocs = disableXmlDocs;
4445
UnreferencedTypesHandling = unreferencedTypesHandling;
4546
LicenseInfo = licenseInfo;
47+
PluginPaths = pluginPaths;
4648
}
4749

4850
/// <summary>
@@ -53,6 +55,7 @@ private static class Options
5355
public const string PackageName = "package-name";
5456
public const string DisableXmlDocs = "disable-xml-docs";
5557
public const string UnreferencedTypesHandling = "unreferenced-types-handling";
58+
public const string Plugins = "plugins";
5659
}
5760

5861
/// <summary>
@@ -86,6 +89,13 @@ private static class Options
8689

8790
public string PackageName { get; }
8891

92+
/// <summary>
93+
/// Gets the paths to plugin assemblies (DLLs) or directories containing plugin assemblies.
94+
/// When specified, the generator loads plugins from these paths in addition to any
95+
/// plugins discovered via node_modules.
96+
/// </summary>
97+
public IReadOnlyList<string>? PluginPaths { get; }
98+
8999
/// <summary>
90100
/// True if a sample project should be generated.
91101
/// </summary>
@@ -123,7 +133,8 @@ internal static Configuration Load(string outputPath, string? json = null)
123133
ReadRequiredStringOption(root, Options.PackageName),
124134
ReadOption(root, Options.DisableXmlDocs),
125135
ReadEnumOption<UnreferencedTypesHandlingOption>(root, Options.UnreferencedTypesHandling),
126-
ReadLicenseInfo(root));
136+
ReadLicenseInfo(root),
137+
ReadStringArrayOption(root, Options.Plugins));
127138
}
128139

129140
private static LicenseInfo? ReadLicenseInfo(JsonElement root)
@@ -164,6 +175,7 @@ internal static Configuration Load(string outputPath, string? json = null)
164175
Options.PackageName,
165176
Options.DisableXmlDocs,
166177
Options.UnreferencedTypesHandling,
178+
Options.Plugins,
167179
};
168180

169181
private static bool ReadOption(JsonElement root, string option)
@@ -191,6 +203,25 @@ private static string ReadRequiredStringOption(JsonElement root, string option)
191203
return null;
192204
}
193205

206+
private static IReadOnlyList<string>? ReadStringArrayOption(JsonElement root, string option)
207+
{
208+
if (root.TryGetProperty(option, out JsonElement value) && value.ValueKind == JsonValueKind.Array)
209+
{
210+
var list = new List<string>();
211+
foreach (var item in value.EnumerateArray())
212+
{
213+
var str = item.GetString();
214+
if (!string.IsNullOrEmpty(str))
215+
{
216+
list.Add(str);
217+
}
218+
}
219+
return list.Count > 0 ? list : null;
220+
}
221+
222+
return null;
223+
}
224+
194225
/// <summary>
195226
/// Returns the default value for the given option.
196227
/// </summary>

0 commit comments

Comments
 (0)