Skip to content

Commit 49dbb4b

Browse files
authored
feat(csharp): hand emitters over on eject and make api-docs ejectable (#432)
* feat(csharp): eject hands over the emitters a generator writes through; api-docs is ejectable An ejected C# generator used to be one file over packaged emitters, so a defect in the enum, navigation, doc-comment or prompt-tier writers still waited for a release. Eject now copies those emitters with the generator, into codegen/generators/ in the same Codegen.Generators namespace: Fr019SharedEnum, XmlDocBuilder, M2MNavigation and ReverseFinders with entity, db-context and routes as each calls them; ExtractDelegateEmitter, Fr010FieldMapping and OutputFormatSpecEmitter with output-parser, extractor and output-prompt. The list is each registry entry's Companions. A file two generators share is copied once and never overwritten. Naming and type-mapping tables, the model walks and the generator base stay in the package as the engine. api-docs is registered for C# and ejectable. The api reference is now a generator (IDocsGenerator) that `dotnet meta docs` runs; `gen` and `verify --codegen` skip it and say so. `docs` hands off to an owned codegen project once it owns api-docs, and what the reference documents comes from the generators in that project's list (GeneratorApplicability.From), so it describes an owned copy. Moved to the engine so an owned copy needs no emitter: CSharpStringLiteral to CSharpNaming, ValueObjectNames.ResolveValueObject out of RenderHelperGenerator. EjectedGeneratorsAloneTests derives its list from the registry: each ejectable generator is ejected alone, compiled against the public API, checked against a written engine surface, and compared byte for byte with the packaged generator. Packaged output is unchanged. * no-mistakes(review): Fix C# eject review findings: engine guard, pin warning * no-mistakes(review): Warn on skipped docs generator only when selected; fix pin wording * docs(csharp): api-docs note says gen warns only when --generators names it The registry Note shown by gen --list and the matching manifest note still said a gen run that lists api-docs warns. After the gen change, an owned project that lists it in codegen/Program.cs is silent; only a --generators selection naming it warns. Text only.
1 parent 0f8a14b commit 49dbb4b

42 files changed

Lines changed: 1843 additions & 378 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,62 @@ generator catalog and the Atlas evaluation are 1.2 work.
138138
`GeneratorRegistry.GeneratorInfo.ejectWith()`, the classes eject copies with a
139139
generator.
140140

141+
- **C#: an ejected generator owns the emitters it writes through, and `api-docs` can be
142+
ejected.** `dotnet meta eject` used to copy one file per generator, so an owned `entity`
143+
still called four packaged writers, and an owned `output-parser`, `extractor` or
144+
`output-prompt` still called the prompt tier's field mapping and format-spec writer. A
145+
defect in any of them waited for a release. Eject now hands
146+
over the writers with the generator, into `codegen/generators/`, in the same
147+
`Codegen.Generators` namespace: `Fr019SharedEnum`, `XmlDocBuilder`, `M2MNavigation` and
148+
`ReverseFinders` with `entity`, `db-context` and `routes` as each calls them;
149+
`ExtractDelegateEmitter`, `Fr010FieldMapping` and `OutputFormatSpecEmitter` with
150+
`output-parser`, `extractor` and `output-prompt`. A file two generators share is copied once
151+
and never overwritten (`--force` replaces it, as it replaces the generator). What stays in
152+
the package is the engine: the generator base, `CSharpNaming`, `ValueObjectNames` and
153+
`PackageBindingResolver` (the naming and type-mapping tables), and the questions asked of
154+
the model (`FindInbound`, `TphPlan`, `InstanceArtifacts`, `ReportRows`). `template` is not
155+
ejectable: it has no emit logic of its own. Packaged output is unchanged and gated to be.
156+
- **`api-docs` is a generator you can own.** `dotnet meta eject api-docs` copies the page
157+
generator with its page model, builder, renderer and path math. `dotnet meta docs` hands
158+
off to `dotnet run --project codegen -- docs` once `codegen/generators/ApiDocsGenerator.cs`
159+
exists, so your copy writes the reference. What the reference documents is still what the
160+
packaged generators emit for (`GeneratorApplicability.Packaged`): an owned `routes` whose
161+
`AppliesTo` you changed is documented as the packaged one behaves, unless you edit the
162+
owned builder.
163+
`dotnet meta gen` and `verify --codegen` leave a docs generator out. `gen` says so only
164+
when a `--generators` selection names `api-docs`; an owned `Program.cs` that lists it runs
165+
without a warning.
166+
New public API in `MetaObjects.Codegen`: `IDocsGenerator`, `DocsRunFacts`,
167+
`GeneratorApplicability`, `GenContext.Applicability` and `GenContext.Docs`,
168+
`CodegenCli.RunDocs`, and `ValueObjectNames.ResolveValueObject` (moved from
169+
`RenderHelperGenerator`, where it was `internal`). `CSharpStringLiteral` moved from
170+
`Fr010FieldMapping` to `CSharpNaming`, so `names` needs no emitter; the old member stays as an
171+
`[Obsolete]` forwarder (see **Deprecated**).
172+
- **A copy you ejected before this release** keeps compiling and running: it finds the
173+
packaged emitters as it always did. `dotnet meta gen --list` now marks it as missing the
174+
ones it was not handed (`Fr019SharedEnum.cs missing`). To own them, first raise the
175+
`MetaObjects.Codegen` version in `codegen/Codegen.csproj` to 1.1.0 or later (the 1.1.0
176+
emitter files are written against the 1.1.0 package, and some call API 1.0.x does not
177+
have), then eject the same name
178+
into a scratch directory (`dotnet meta eject entity --root <scratch>`) and copy the emitter
179+
files from its `codegen/generators/` into yours; your copy and your edits are untouched,
180+
and it resolves the new files by namespace.
181+
- **Eject warns about an older package pin.** Eject writes the sources of the tool you run
182+
and never edits `codegen/Codegen.csproj`. When that file pins a `MetaObjects.Codegen` older
183+
than the tool, `dotnet meta eject` now prints a warning with the pinned version, the
184+
tool's version and the line to change; the owned project does not compile until the pin
185+
is raised. A 1.0.x project needs this before `dotnet meta eject api-docs`, and before
186+
re-ejecting `names`, `render-helper` or a prompt-tier generator.
187+
- **A gate for the rule.** For every ejectable generator the registry lists, a test ejects it
188+
alone, compiles it against the public API, fails on any reference to a packaged emitter,
189+
generator or docs writer outside a written engine surface, fails on a handed-over file
190+
nothing calls, and compares its output with the packaged generator's over the shared
191+
corpora. A registered generator with no eject source and no written exemption fails it, as
192+
does a generator class the package ships that no registry entry builds. A second test
193+
compiles the package without the handed-over emitters, so nothing that stays packaged
194+
calls one.
195+
The cross-port manifest lists `api-docs` for `csharp`.
196+
141197
- **The stock `entity`, `queries`, `routes` and `barrel` generators are importable again
142198
(TypeScript).** `entityFile`, `queriesFile`, `routesFile` and `barrel` are exported from
143199
`@metaobjectsdev/codegen-ts/generators` once more, beside `namesFile`, `routesFileHono`
@@ -744,6 +800,12 @@ until you regenerate.
744800
One rule is relaxed: an authored `validator.length @min: 0` on a `@required` string now
745801
admits the empty string, as the generated schema already did.
746802

803+
### Deprecated
804+
805+
- **C#: `Fr010FieldMapping.CSharpStringLiteral(string)` is `[Obsolete]`.** It forwards to
806+
`CSharpNaming.CSharpStringLiteral(string)`, which every generator now calls (so an ejected
807+
`names` needs no emitter). Same signature and result; switch the call when you next touch it.
808+
747809
### Removed
748810

749811
- **The standalone `mermaid-er` generator (TypeScript).** `mermaidErDiagram()`, its

‎docs/features/own-your-codegen.md‎

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,12 +1014,18 @@ ejectable generator's `.cs` file ships embedded in `MetaObjects.Codegen`, and a
10141014
byte-identical to the file the package compiles. Eject writes it to
10151015
`codegen/generators/<Name>Generator.cs`, renaming its namespace to `Codegen.Generators` and
10161016
adding the `using` lines that rename needs; nothing else changes, except in `routes` (see
1017-
[below](#ejecting-routes-hands-over-the-helper-runtime-too)).
1017+
[below](#ejecting-routes-hands-over-the-helper-runtime-too)). The emitters the generator
1018+
writes its output through come with it
1019+
([below](#ejecting-a-generator-hands-over-the-emitters-it-writes-through)).
10181020

10191021
On first use eject also writes `codegen/Codegen.csproj`, a console project referencing
10201022
`MetaObjects.Codegen` at the tool's version, and `codegen/Program.cs`, which lists your
10211023
generators with `new` and calls the public runner. After that, both files are yours: eject
1022-
never touches them again, even with `--force`, and prints what to add instead. Once
1024+
never touches them again, even with `--force`, and prints what to add instead. That includes
1025+
the package version: eject writes the sources of the tool you run, so when
1026+
`codegen/Codegen.csproj` pins an older `MetaObjects.Codegen` than the tool, eject prints a
1027+
warning with the pinned version, the tool's version and the line to change. The owned
1028+
project does not compile until the pin is the tool's version or later. Once
10231029
`codegen/Codegen.csproj` exists, `dotnet meta gen` and `dotnet meta verify --codegen` hand
10241030
off to `dotnet run --project codegen` with the same arguments, so your owned generators
10251031
run everywhere the tool did.
@@ -1031,14 +1037,95 @@ them. So ejecting `names` from a nine-generator selection changes one generator'
10311037
not the other eight. The owned project finds metadata exactly as the tool does, including
10321038
`.metaobjects/config.json`'s `sources` and `libraries`. Eject never overwrites a copy without `--force`, and
10331039
`dotnet meta gen --list` marks owned copies `identical` or `DIFFERS: N behind, M of your
1034-
own`. The `template` primitive is not ejectable; it has no emit logic of its own.
1040+
own`, and names an emitter that was edited or removed after eject
1041+
(`Fr019SharedEnum.cs DIFFERS: 0 behind, 1 of your own`). The `template` primitive is not
1042+
ejectable; it has no emit logic of its own.
10351043
10361044
`dotnet meta eject requirement-tests` copies `RequirementTestsGenerator.cs`, which holds the
10371045
generator and its default rendering. Its options are public properties you set in
10381046
`codegen/Program.cs` (`new RequirementTestsGenerator { WitnessClass = "…" }`); the hook
10391047
types (`IRequirementTestRenderer`, `IRequirementTestFilter`), the identity function and the
10401048
digest stay in the package.
10411049
1050+
### Ejecting a generator hands over the emitters it writes through
1051+
1052+
A generator that calls an emitter writes part of its output through it. If eject left the
1053+
emitter in the package, the copy would own the orchestration and the package would still own
1054+
the text, and a defect in that text would wait for a release. So eject copies the emitters
1055+
too, next to the generator in `codegen/generators/`, in the same `Codegen.Generators`
1056+
namespace:
1057+
1058+
| Eject | Also writes |
1059+
|---|---|
1060+
| `entity` | `Fr019SharedEnum.cs` (shared and provided enums), `M2MNavigation.cs` (the many-to-many navigation), `ReverseFinders.cs` (the reverse FK finders), `XmlDocBuilder.cs` (doc comments) |
1061+
| `db-context` | `Fr019SharedEnum.cs`, `M2MNavigation.cs`, `XmlDocBuilder.cs` |
1062+
| `routes` | `M2MNavigation.cs` |
1063+
| `output-parser`, `extractor` | `ExtractDelegateEmitter.cs`, `Fr010FieldMapping.cs`; `extractor` also `Fr019SharedEnum.cs` |
1064+
| `output-prompt` | `OutputFormatSpecEmitter.cs`, `Fr010FieldMapping.cs` |
1065+
| `api-docs` | the page model, builder, renderer and path math (below), `Fr010FieldMapping.cs`, `M2MNavigation.cs` |
1066+
1067+
The copies are the packaged source with the same namespace edit as the generator and no
1068+
other change. A file two generators share is written once: a later eject finds it, says
1069+
`[kept — already yours]` and leaves your edits alone, and `--force` replaces it with the
1070+
reference as it replaces the generator. The copies see one another as members of their own
1071+
namespace, and a member of the namespace you are in wins over a name a `using` brings in, so
1072+
an owned generator reaches its owned emitters and never the packaged ones of the same name.
1073+
1074+
**What stays in the package is the engine**, public and unchanged: the generator contract
1075+
(`IGenerator`, `GenContext`, `GenConfig`, `EmittedFile`), the naming and type-mapping tables
1076+
(`CSharpNaming`, `ValueObjectNames`, `PackageBindingResolver`), and the questions a generator
1077+
asks of the model (`FindInbound`, `TphPlan`, `InstanceArtifacts`, `ReportRows`). Anything that
1078+
returns emitted text is handed over; a table or a model question is not. The line is written
1079+
down and checked: a test ejects each generator the registry lists into an empty project,
1080+
compiles it against the public API and fails on any reference to a packaged emitter, generator
1081+
or docs writer that is not on that list. A second test compiles the package without the
1082+
handed-over emitters, so nothing that stays packaged calls one.
1083+
1084+
**A copy you ejected before the emitters were handed over** keeps compiling and running, with
1085+
the packaged emitters. To own them, first raise the `MetaObjects.Codegen` version in
1086+
`codegen/Codegen.csproj` to 1.1.0 or later: the 1.1.0 emitter files are written against the
1087+
1.1.0 package, and some call API that 1.0.x does not have. Then eject the same name
1088+
into a scratch directory and copy the emitter files into your project; your copy and your
1089+
edits are untouched:
1090+
1091+
```bash
1092+
scratch=$(mktemp -d)
1093+
dotnet meta eject entity --root "$scratch"
1094+
cp "$scratch"/codegen/generators/{Fr019SharedEnum,M2MNavigation,ReverseFinders,XmlDocBuilder}.cs codegen/generators/
1095+
dotnet meta gen --list # no longer reports an emitter missing
1096+
```
1097+
1098+
### `api-docs`: the api reference as an owned generator
1099+
1100+
`dotnet meta docs` writes the C# api reference. `dotnet meta eject api-docs` copies the
1101+
generator that writes it, with the page model (`CSharpApiModel.cs`), the builder that decides
1102+
what to document (`CSharpApiModelBuilder.cs`), the renderer (`CSharpApiDocsRenderer.cs`) and
1103+
the page-path math (`DocsPaths.cs`). Change which nodes get a page, how a page reads, or
1104+
where it lands, by editing them.
1105+
1106+
Once `codegen/generators/ApiDocsGenerator.cs` exists, `dotnet meta docs` hands off to
1107+
`dotnet run --project codegen -- docs` with the same arguments, and the reference is written
1108+
by your copy. A project that owns `codegen/` but not `api-docs` keeps the packaged reference:
1109+
the hand-off for `docs` is keyed to the generator, not to the project, because an owned
1110+
project pinned to an older `MetaObjects.Codegen` does not know the `docs` command. The owned
1111+
copy needs `MetaObjects.Codegen` 1.1.0 or later for the same reason; eject warns when
1112+
`codegen/Codegen.csproj` pins less.
1113+
1114+
**It describes what the packaged generators emit.** The reference documents a route, an
1115+
allowlist, a render helper only if the packaged generator that emits it would, which each
1116+
answers through its static `AppliesTo`. That holds in a project that owns one of those
1117+
generators too: an owned `routes` whose `AppliesTo` you changed is still documented as the
1118+
packaged one behaves. To document your own surface, edit the owned builder
1119+
(`CSharpApiModelBuilder.cs`), which is where the question is asked. The builder asks through
1120+
`GenContext.Applicability` (`GeneratorApplicability.Packaged`), never through a generator
1121+
class, which is why the copy depends on nothing but the engine.
1122+
1123+
`Program.cs` lists `new ApiDocsGenerator()` beside the code generators. `dotnet meta gen` and
1124+
`verify --codegen` skip it: it writes pages under the docs directory, not code under `--out`.
1125+
The skip is silent while the generator is only in that list. When a `--generators` selection
1126+
names `api-docs`, `gen` says so (`"api-docs" writes the api reference, so gen skipped it.
1127+
Run it with dotnet meta docs.`), with or without an owned project.
1128+
10421129
### Ejecting routes hands over the helper runtime too
10431130
10441131
Generated routes call a handful of helpers: the filter parser, the EF Core filter

‎docs/ports/csharp.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ for a worked example.
8181

8282
## Generate
8383

84-
**Own a generator.** `dotnet meta eject <name>...` copies a reference generator into `codegen/generators/` and scaffolds an owned `codegen/` console project that `dotnet meta gen` then runs. See [Own your codegen → C#](../features/own-your-codegen.md#c-dotnet-meta-eject).
84+
**Own a generator.** `dotnet meta eject <name>...` copies a reference generator into `codegen/generators/` and scaffolds an owned `codegen/` console project that `dotnet meta gen` then runs. Every generator `dotnet meta gen --list` shows is ejectable except `template`, the Mustache primitive, which has no emit logic of its own to own. An ejected generator comes with the emitters it writes its output through (the enum, navigation, doc-comment and prompt-tier writers), so the copy owns the code that writes the file and not a shell around the package's; the naming and type-mapping tables (`CSharpNaming`, `ValueObjectNames`) stay in the package as the engine. `dotnet meta eject api-docs` does the same for the api reference `dotnet meta docs` writes. Ejecting into a project whose `codegen/Codegen.csproj` pins an older `MetaObjects.Codegen` than the tool prints a warning naming the line to change. See [Own your codegen → C#](../features/own-your-codegen.md#c-dotnet-meta-eject).
8585

8686
```bash
8787
# Generate EF Core entities + AppDbContext + CRUD minimal-API routes

‎fixtures/generator-registry-conformance/registry.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@
9090
"concept": "Per-entity/template SDK API reference (generated-code API; human + agent forms).",
9191
"tier": "native",
9292
"layer": "docs",
93-
"note": "Run by `meta docs` as its api surface (ADR-0025), not by `meta gen`: a `meta gen` config that lists it is warned about and skipped.",
94-
"ports": ["typescript"]
93+
"note": "Run by `meta docs` as its api surface (ADR-0025), not by `meta gen`: a `meta gen` config that lists it is warned about and skipped. On csharp it is run by `dotnet meta docs`, and `dotnet meta gen` skips it, warning only when `--generators` names it.",
94+
"ports": ["typescript", "csharp"]
9595
},
9696
"trace-helper": {
9797
"concept": "Per-entity typed record<Entity>/call<Entity> trace helpers (extract + buildLlmCallRow + persist; LlmCallBase-derived entities only).",

0 commit comments

Comments
 (0)