Skip to content

Commit 3cc7182

Browse files
authored
Add llms.txt generation from XML docs and update build (#162)
1 parent b26d6fa commit 3cc7182

File tree

3 files changed

+18000
-0
lines changed

3 files changed

+18000
-0
lines changed

Makefile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ help:
6565
@printf ' $(C_CYN)%-12s$(C_RST) - %s\n' 'publish' 'Publish the Thirdweb project (dotnet publish)'
6666
@printf ' $(C_CYN)%-12s$(C_RST) - %s\n' 'run' 'Run the console application'
6767
@printf ' $(C_CYN)%-12s$(C_RST) - %s\n' 'generate' 'Generate API client from OpenAPI spec'
68+
@printf ' $(C_CYN)%-12s$(C_RST) - %s\n' 'generate-llms' 'Generate llms.txt from XML documentation'
6869
@printf ' $(C_CYN)%-12s$(C_RST) - %s\n' 'lint' 'Check code formatting (dry run)'
6970
@printf ' $(C_CYN)%-12s$(C_RST) - %s\n' 'fix' 'Fix code formatting issues'
7071
@printf ' $(C_CYN)%-12s$(C_RST) - %s\n' 'help' 'Show this help message'
@@ -105,13 +106,27 @@ generate:
105106
)
106107
@$(call msg,$(C_GRN),$(IC_OK),API client generation complete)
107108

109+
.PHONY: generate-llms
110+
# Generate llms.txt from XML documentation
111+
generate-llms:
112+
@$(call msg,$(C_BLU),$(IC_INFO),$(IC_BUILD) Building Thirdweb in Release mode)
113+
@$(DOTNET) build '$(LIB_PROJ)' -c Release >/dev/null 2>&1 || { \
114+
$(call msg,$(C_MAG),>> ,Building Thirdweb project) ; \
115+
$(DOTNET) build '$(LIB_PROJ)' -c Release ; \
116+
}
117+
@$(call msg,$(C_BLU),$(IC_INFO),$(IC_GEN) Generating llms.txt from XML documentation)
118+
@$(DOTNET) run --project '$(GENERATOR_PROJ)' -- --llms && \
119+
$(call msg,$(C_GRN),$(IC_OK),llms.txt generation complete) || \
120+
$(call msg,$(C_RED),$(IC_ERR),llms.txt generation failed)
121+
108122
.PHONY: build
109123
build:
110124
@$(MAKE) --no-print-directory generate
111125
@$(call msg,$(C_BLU),$(IC_INFO),$(IC_BUILD) Building with dotnet build)
112126
@$(DOTNET) build && \
113127
$(call msg,$(C_GRN),$(IC_OK),Build succeeded) || \
114128
$(call msg,$(C_RED),$(IC_ERR),Build failed)
129+
@$(MAKE) --no-print-directory generate-llms
115130

116131
.PHONY: clean
117132
clean:

Thirdweb.Generator/Program.cs

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System.Globalization;
2+
using System.Text;
3+
using System.Xml.Linq;
24
using NJsonSchema;
35
using NSwag;
46
using NSwag.CodeGeneration.CSharp;
@@ -18,6 +20,270 @@ static string FindRepoRoot()
1820
return Directory.GetCurrentDirectory();
1921
}
2022

23+
static void GenerateLlmsTxt(string repoRoot)
24+
{
25+
var xmlPath = Path.Combine(repoRoot, "Thirdweb", "bin", "Release", "netstandard2.1", "Thirdweb.xml");
26+
var outputPath = Path.Combine(repoRoot, "llms.txt");
27+
28+
if (!File.Exists(xmlPath))
29+
{
30+
Console.WriteLine($"XML documentation not found at {xmlPath}");
31+
Console.WriteLine("Please build the project in Release mode first.");
32+
Environment.Exit(1);
33+
}
34+
35+
Console.WriteLine($"Reading XML documentation from {xmlPath}...");
36+
var doc = XDocument.Load(xmlPath);
37+
var sb = new StringBuilder();
38+
39+
_ = sb.AppendLine("THIRDWEB .NET SDK - API DOCUMENTATION");
40+
_ = sb.AppendLine("=====================================");
41+
_ = sb.AppendLine();
42+
43+
var assembly = doc.Root?.Element("assembly")?.Element("name")?.Value;
44+
if (assembly != null)
45+
{
46+
_ = sb.AppendLine($"Assembly: {assembly}");
47+
_ = sb.AppendLine();
48+
}
49+
50+
var members = doc.Root?.Element("members")?.Elements("member");
51+
if (members != null)
52+
{
53+
foreach (var member in members)
54+
{
55+
var name = member.Attribute("name")?.Value;
56+
if (string.IsNullOrEmpty(name))
57+
{
58+
continue;
59+
}
60+
61+
_ = sb.AppendLine(new string('-', 80));
62+
_ = sb.AppendLine(name);
63+
_ = sb.AppendLine(new string('-', 80));
64+
65+
// Parse signature for better display
66+
var signature = ParseMemberSignature(name);
67+
if (signature != null)
68+
{
69+
_ = sb.AppendLine();
70+
_ = sb.AppendLine($"KIND: {signature.Kind}");
71+
if (!string.IsNullOrEmpty(signature.ReturnType))
72+
{
73+
_ = sb.AppendLine($"RETURN TYPE: {signature.ReturnType}");
74+
}
75+
}
76+
77+
// Group param elements by name attribute
78+
var paramDocs = member.Elements("param").Select(p => new { Name = p.Attribute("name")?.Value, Description = p.Value.Trim() }).Where(p => !string.IsNullOrEmpty(p.Name)).ToList();
79+
80+
// Display parameters with their names and types
81+
if (signature?.Parameters != null && signature.Parameters.Count > 0)
82+
{
83+
_ = sb.AppendLine();
84+
_ = sb.AppendLine("PARAMETERS:");
85+
for (var i = 0; i < signature.Parameters.Count; i++)
86+
{
87+
var param = signature.Parameters[i];
88+
// Try to get the actual parameter name from documentation
89+
var paramDoc = i < paramDocs.Count ? paramDocs[i] : null;
90+
var paramName = paramDoc?.Name ?? param.Name;
91+
92+
_ = sb.AppendLine($" - {paramName} ({param.Type})");
93+
if (paramDoc != null && !string.IsNullOrEmpty(paramDoc.Description))
94+
{
95+
_ = sb.AppendLine($" {NormalizeXmlText(paramDoc.Description).Replace("\n", "\n ")}");
96+
}
97+
}
98+
}
99+
100+
// Display other elements (summary, remarks, returns, exception, etc.)
101+
foreach (var element in member.Elements())
102+
{
103+
if (element.Name.LocalName == "param")
104+
{
105+
continue; // Already handled above
106+
}
107+
108+
var content = element.Value.Trim();
109+
if (string.IsNullOrEmpty(content))
110+
{
111+
continue;
112+
}
113+
114+
_ = sb.AppendLine();
115+
var elementName = element.Name.LocalName.ToUpper();
116+
117+
// Add attribute info for exceptions and type params
118+
var nameAttr = element.Attribute("name")?.Value;
119+
var crefAttr = element.Attribute("cref")?.Value;
120+
if (!string.IsNullOrEmpty(nameAttr))
121+
{
122+
elementName += $" ({nameAttr})";
123+
}
124+
else if (!string.IsNullOrEmpty(crefAttr))
125+
{
126+
elementName += $" ({crefAttr})";
127+
}
128+
129+
_ = sb.AppendLine($"{elementName}:");
130+
_ = sb.AppendLine(NormalizeXmlText(content));
131+
}
132+
133+
_ = sb.AppendLine();
134+
}
135+
}
136+
137+
File.WriteAllText(outputPath, sb.ToString());
138+
Console.WriteLine($"Generated llms.txt at {outputPath}");
139+
}
140+
141+
static MemberSignature? ParseMemberSignature(string memberName)
142+
{
143+
if (string.IsNullOrEmpty(memberName) || memberName.Length < 2)
144+
{
145+
return null;
146+
}
147+
148+
var kind = memberName[0] switch
149+
{
150+
'M' => "Method",
151+
'P' => "Property",
152+
'T' => "Type",
153+
'F' => "Field",
154+
'E' => "Event",
155+
_ => "Unknown",
156+
};
157+
158+
var fullSignature = memberName[2..]; // Remove "M:", "P:", etc.
159+
160+
// For methods, parse parameters and return type
161+
if (memberName[0] == 'M')
162+
{
163+
var parameters = new List<ParameterInfo>();
164+
var methodName = fullSignature;
165+
var returnType = "System.Threading.Tasks.Task"; // Default for async methods
166+
167+
// Extract parameters from signature
168+
var paramStart = fullSignature.IndexOf('(');
169+
if (paramStart >= 0)
170+
{
171+
methodName = fullSignature[..paramStart];
172+
var paramEnd = fullSignature.LastIndexOf(')');
173+
if (paramEnd > paramStart)
174+
{
175+
var paramString = fullSignature[(paramStart + 1)..paramEnd];
176+
if (!string.IsNullOrEmpty(paramString))
177+
{
178+
var paramParts = SplitParameters(paramString);
179+
for (var i = 0; i < paramParts.Count; i++)
180+
{
181+
var paramType = SimplifyTypeName(paramParts[i]);
182+
parameters.Add(new ParameterInfo { Name = $"param{i + 1}", Type = paramType });
183+
}
184+
}
185+
}
186+
187+
// Check if there's a return type after the parameters
188+
if (paramEnd + 1 < fullSignature.Length && fullSignature[paramEnd + 1] == '~')
189+
{
190+
returnType = SimplifyTypeName(fullSignature[(paramEnd + 2)..]);
191+
}
192+
}
193+
194+
return new MemberSignature
195+
{
196+
Kind = kind,
197+
Parameters = parameters,
198+
ReturnType = parameters.Count > 0 || fullSignature.Contains("Async") ? returnType : null,
199+
};
200+
}
201+
202+
return new MemberSignature { Kind = kind };
203+
}
204+
205+
static List<string> SplitParameters(string paramString)
206+
{
207+
var parameters = new List<string>();
208+
var current = new StringBuilder();
209+
var depth = 0;
210+
211+
foreach (var c in paramString)
212+
{
213+
if (c is '{' or '<')
214+
{
215+
depth++;
216+
}
217+
else if (c is '}' or '>')
218+
{
219+
depth--;
220+
}
221+
else if (c == ',' && depth == 0)
222+
{
223+
parameters.Add(current.ToString());
224+
_ = current.Clear();
225+
continue;
226+
}
227+
228+
_ = current.Append(c);
229+
}
230+
231+
if (current.Length > 0)
232+
{
233+
parameters.Add(current.ToString());
234+
}
235+
236+
return parameters;
237+
}
238+
239+
static string SimplifyTypeName(string fullTypeName)
240+
{
241+
// Remove assembly information
242+
var typeName = fullTypeName.Split(',')[0];
243+
244+
// Simplify common generic types
245+
typeName = typeName
246+
.Replace("System.Threading.Tasks.Task{", "Task<")
247+
.Replace("System.Collections.Generic.List{", "List<")
248+
.Replace("System.Collections.Generic.Dictionary{", "Dictionary<")
249+
.Replace("System.Nullable{", "Nullable<")
250+
.Replace("System.String", "string")
251+
.Replace("System.Int32", "int")
252+
.Replace("System.Int64", "long")
253+
.Replace("System.Boolean", "bool")
254+
.Replace("System.Byte", "byte")
255+
.Replace("System.Object", "object")
256+
.Replace('{', '<')
257+
.Replace('}', '>');
258+
259+
return typeName;
260+
}
261+
262+
static string NormalizeXmlText(string text)
263+
{
264+
var lines = text.Split('\n');
265+
var normalized = new StringBuilder();
266+
267+
foreach (var line in lines)
268+
{
269+
var trimmed = line.Trim();
270+
if (!string.IsNullOrEmpty(trimmed))
271+
{
272+
_ = normalized.AppendLine(trimmed);
273+
}
274+
}
275+
276+
return normalized.ToString().TrimEnd();
277+
}
278+
279+
var cmdArgs = Environment.GetCommandLineArgs();
280+
if (cmdArgs.Length > 1 && cmdArgs[1] == "--llms")
281+
{
282+
var root = FindRepoRoot();
283+
GenerateLlmsTxt(root);
284+
return;
285+
}
286+
21287
var specUrl = "https://api.thirdweb.com/openapi.json";
22288
var repoRoot = FindRepoRoot();
23289
var outputPath = Path.Combine(repoRoot, "Thirdweb", "Thirdweb.Api", "ThirdwebApi.cs");
@@ -177,3 +443,16 @@ void DedupeEnumOnSchema(JsonSchema schema, string? debugPath)
177443
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
178444
await File.WriteAllTextAsync(outputPath, code);
179445
Console.WriteLine($"Wrote generated client to {outputPath}");
446+
447+
internal class MemberSignature
448+
{
449+
public string Kind { get; set; } = "";
450+
public List<ParameterInfo>? Parameters { get; set; }
451+
public string? ReturnType { get; set; }
452+
}
453+
454+
internal class ParameterInfo
455+
{
456+
public string Name { get; set; } = "";
457+
public string Type { get; set; } = "";
458+
}

0 commit comments

Comments
 (0)