Skip to content
Open
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
29 changes: 29 additions & 0 deletions docs/src/content/docs/packages/typegen/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,35 @@ stale files in the touched directories: any file carrying our `@generated`
banner that isn't in the current emit set gets deleted, so renames don't
leak orphaned outputs.

### Removing the last generation request

When a successful generation pass has no output, TypeGen still emits a valid
manifest with `FileCount = 0`. Removing the final `[GenerateTypes]` attribute
or fluent `.WithGeneratedTypes(...)` registration therefore replaces the
previous manifest rather than leaving it available for MSBuild to replay.
An emitter that legitimately produces no files follows the same path.

Both live writes and the MSBuild task use `obj/.typegen-dirs` to find previously
used output directories and remove files bearing the TypeGen banner. Handwritten
files are preserved. Subsequent builds cannot resurrect old output from the
empty manifest, even when compilation is skipped. Compilation errors or fatal
TypeGen diagnostics that prevent generation are not treated as successful empty
runs and do not authorize this cleanup.

After upgrading from a version that retained stale manifests, run
`dotnet build -t:Rebuild` once for each affected project to replace its cached
manifest. A configurator in another project does not update this project's
manifest. If the TypeGen package has already been removed entirely, its generator
and cleanup task cannot run; clear the affected project's old `obj` cache and
remove obsolete generated files manually.

Live cleanup requires an IDE host that reruns the generator and permits analyzer
file I/O; otherwise build the project to run the MSBuild cleanup. Files generated
with banners disabled are not automatically removed. Ownership remains
banner-based and scoped to directories rather than individual projects: use
separate output directories for different projects, since sharing one can cause
one project's sweep to remove another project's generated files.

No companion task DLL, no reflection over the built assembly — the inline task is ~60
lines of C# directly in the `.targets` file. Files are skipped when their content is
unchanged (stable mtimes, no file-watcher thrash in frontend dev servers).
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var (((symbols, compilation), projectDir), _configuratorSources) = source;
// Note: do NOT early-return on empty symbols — the configurator's
// .WithGeneratedTypes(...) fluent discovery (handled below) is a valid
// sole driver of generation. The bottom of this callback exits anyway
// if nothing accumulates in the model.
// sole driver of generation. A successful empty run must replace the
// persisted manifest too, otherwise MSBuild replays the previous run.
if (symbols.IsDefault) symbols = ImmutableArray<INamedTypeSymbol?>.Empty;

bool hasGenerationErrors = false;
void ReportDiagnostic(Diagnostic diagnostic)
{
if (diagnostic.Severity == DiagnosticSeverity.Error) hasGenerationErrors = true;
spc.ReportDiagnostic(diagnostic);
}

// Warn about [CrudApi]-only classes that TypeGen can't emit paths for —
// they need [GenerateTypes] so we discover them via the same provider.
// (Actually they're already in `symbols` thanks to the dual predicate above,
Expand All @@ -89,15 +96,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
{
if (sym is null) continue;
if (!SchemaParser.HasGenerateTypes(sym) && SchemaParser.HasCrudApi(sym))
spc.ReportDiagnostic(Diagnostic.Create(
ReportDiagnostic(Diagnostic.Create(
TypeGenDiagnostics.CrudApiWithoutGenerateTypes,
sym.Locations.FirstOrDefault() ?? Location.None,
sym.ToDisplayString()));
}

// Parse ITypeGenConfigurator DSL first — global settings feed the emitters,
// per-type overrides merge into SchemaClass/SchemaEnum below.
var config = ConfiguratorParser.Parse(compilation, spc.ReportDiagnostic);
var config = ConfiguratorParser.Parse(compilation, ReportDiagnostic);

var model = new SchemaModel();
foreach (var sym in symbols)
Expand All @@ -108,7 +115,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
// instead of emitting broken output.
if (sym.IsGenericType)
{
spc.ReportDiagnostic(Diagnostic.Create(
ReportDiagnostic(Diagnostic.Create(
TypeGenDiagnostics.GenericType,
sym.Locations.FirstOrDefault() ?? Location.None,
sym.ToDisplayString()));
Expand All @@ -120,17 +127,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var en = SchemaParser.ParseEnum(sym);
if (en is null) continue;
ApplyFluentToEnum(en, config);
ReportTargetsIfEmpty(spc, sym, en.Targets);
ReportInvalidOutputDirIfEmpty(spc, sym, en.OutputDir);
ReportTargetsIfEmpty(ReportDiagnostic, sym, en.Targets);
ReportInvalidOutputDirIfEmpty(ReportDiagnostic, sym, en.OutputDir);
model.Enums.Add(en);
}
else
{
var cls = SchemaParser.ParseClass(sym);
if (cls is null) continue;
ApplyFluentToClass(cls, config);
ReportTargetsIfEmpty(spc, sym, cls.Targets);
ReportInvalidOutputDirIfEmpty(spc, sym, cls.OutputDir);
ReportTargetsIfEmpty(ReportDiagnostic, sym, cls.Targets);
ReportInvalidOutputDirIfEmpty(ReportDiagnostic, sym, cls.OutputDir);
model.Classes.Add(cls);

// Synthesize Create/Update/Response companion schemas from Dto's attributes.
Expand Down Expand Up @@ -312,8 +319,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
// so target names / OutputDirs are final.
SchemaParser.ResolveGenericTypeReferences(model);

if (model.Classes.Count == 0 && model.Enums.Count == 0) return;

// Surface every property whose C# type won't render into a sane TS /
// OpenAPI expression (fell back to `unknown` / `object` because the type
// isn't primitive, isn't in the model, isn't overridden, isn't ignored).
Expand Down Expand Up @@ -349,7 +354,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
if (RequestsTarget(model, TypeTarget.TanStackQuery))
allFiles.AddRange(TanStackQueryEmitter.Emit(model, settings));

if (allFiles.Count == 0) return;
// Empty is an authoritative result only when generation succeeded.
// In particular an incomplete IDE edit / failed configurator must not
// be mistaken for removing the last generation request.
if (allFiles.Count == 0 && (hasGenerationErrors ||
compilation.GetDiagnostics(spc.CancellationToken).Any(d => d.Severity == DiagnosticSeverity.Error)))
return;

// Emit the manifest as a .g.cs containing string constants. The companion
// MSBuild task reads this file at build time, decodes the constants, and
Expand Down Expand Up @@ -699,19 +709,19 @@ private static bool RequestsTarget(SchemaModel model, TypeTarget target)
return false;
}

private static void ReportTargetsIfEmpty(SourceProductionContext spc, ISymbol sym, TypeTarget targets)
private static void ReportTargetsIfEmpty(System.Action<Diagnostic> report, ISymbol sym, TypeTarget targets)
{
if (targets == TypeTarget.None)
spc.ReportDiagnostic(Diagnostic.Create(
report(Diagnostic.Create(
TypeGenDiagnostics.NoTargets,
sym.Locations.FirstOrDefault() ?? Location.None,
sym.Name));
}

private static void ReportInvalidOutputDirIfEmpty(SourceProductionContext spc, ISymbol sym, string outputDir)
private static void ReportInvalidOutputDirIfEmpty(System.Action<Diagnostic> report, ISymbol sym, string outputDir)
{
if (string.IsNullOrWhiteSpace(outputDir))
spc.ReportDiagnostic(Diagnostic.Create(
report(Diagnostic.Create(
TypeGenDiagnostics.InvalidOutputDir,
sym.Locations.FirstOrDefault() ?? Location.None,
outputDir ?? "<null>",
Expand Down
Loading
Loading