In Dart's source_gen, how does SharedPartBuilder let several generators share one .g.dart file, and how do PartBuilder and LibraryBuilder differ?
answer
- pieces first, then one merge
- .partId.g.part in the cache
- combining_builder writes .g.dart
- own extension for PartBuilder
- importable library, one generator
basics
~20 sEach SharedPartBuilder writes a hidden .<partId>.g.part piece to the build cache; source_gen's combining_builder merges all pieces into one .g.dart part. PartBuilder writes its own part extension such as .freezed.dart, and LibraryBuilder writes a standalone importable library.
solid answer
~40 s`source_gen` wraps `Generator` or `GeneratorForAnnotation` classes into `build` package builders. With `SharedPartBuilder(generators, partId)`, output for `person.dart` goes to `person.<partId>.g.part` in the hidden cache, for example `person.json_serializable.g.part`; the builder's `build.yaml` sets `build_to: cache` and `applies_builders: [source_gen|combining_builder]`. The **combining builder** gathers every `.g.part` for that library and writes one `person.g.dart` into the source tree, adding the `// GENERATED CODE - DO NOT MODIFY BY HAND` header, so several packages share one `part 'person.g.dart';`. `PartBuilder` writes a part with an extension unique to its package; freezed uses it for `.freezed.dart`, which needs its own `part` line. `LibraryBuilder` writes a standalone library you `import`, with one generator only. source_gen's docs reserve `.g.dart` for the shared convention.
code
yaml · 9 lines# build.yaml of a generator package using SharedPartBuilder
builders:
json_serializable:
import: "package:json_serializable/builder.dart"
builder_factories: ["jsonSerializable"]
build_extensions: {".dart": ["json_serializable.g.part"]}
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen|combining_builder"]go deeper
Recall that .g.dart can hold code from several generators and that freezed adds a separate .freezed.dart part.
Explain the two-step flow: .g.part pieces in the cache, then combining_builder writing .g.dart, and how PartBuilder and LibraryBuilder differ.
Show you can configure the combined output with header, preamble and ignore_for_file, and diagnose missing-part warnings across generators.
Judge when a team should write its own builder, and which output type keeps it composable with the packages already in use.
## The layers Three packages cooperate when you generate code in Dart: - **`build`** defines the `Builder` interface: given an input file, write declared outputs. - **`build_runner`** runs builders, tracks dependencies between files and decides what to rebuild. - **`source_gen`** sits on top: it gives generator authors a friendlier API (`Generator`, `GeneratorForAnnotation`, `TypeChecker`) built on the analyzer's element model, and wraps generators into one of three builder types. The builder type decides **where the generated code ends up**, and that is what this question is about. ## SharedPartBuilder and the combining step Many popular generators want to add code to the same library: `json_serializable` adds `_$PersonFromJson`, another package might add validation, a third a copy helper. If each wrote its own part file, a user would need one `part` directive per generator. source_gen's convention avoids that: 1. Each generator package builds with **`SharedPartBuilder(generators, partId)`**. For input `person.dart` it writes `person.<partId>.g.part`. json_serializable's `build.yaml` declares `build_extensions: {".dart": ["json_serializable.g.part"]}` and `build_to: cache`, so the piece lands in the **hidden artifact tree**, not in `lib/`. 2. The same `build.yaml` says `applies_builders: ["source_gen|combining_builder"]`, pulling in source_gen's **combining builder**. 3. `combining_builder` has `required_inputs: [".g.part"]`, `build_to: source` and output `.g.dart`. It reads every `.g.part` piece for the library and writes a single `person.g.dart` next to the source, starting with `// GENERATED CODE - DO NOT MODIFY BY HAND` and a `part of` line. 4. A post-process step, `part_cleanup`, removes the intermediate pieces. The user writes one line, `part 'person.g.dart';`, however many shared-part generators contribute. If that directive is missing, the combining step logs a warning naming the `part` line to add. ## Configuring the combined file Because `combining_builder` owns the final file, its options in your `build.yaml` affect every shared-part generator: | Option | Effect | |---|---| | `header` | replace or remove the default `GENERATED CODE` header | | `preamble` | text after the header, such as `// coverage:ignore-file` | | `ignore_for_file` | lint names written as an `ignore_for_file` comment in every generated file | | `build_extensions` | move outputs, for example into `lib/generated/` | ## PartBuilder and LibraryBuilder - **`PartBuilder`** writes a `part of` file with an extension **unique to one package**, and can hold several generators from that package. freezed is built this way: its `build.yaml` declares `.freezed.dart` with `build_to: source`, so a freezed model needs `part 'person.freezed.dart';` as well as `part 'person.g.dart';` when it also uses json_serializable. freezed also declares `runs_before: [json_serializable|json_serializable]` so its output exists when the JSON generator runs. - **`LibraryBuilder`** writes a **standalone library** that other files `import`, not a part. Only a single `Generator` may be used. | Builder | Output | Directive in source | Shared with other packages | |---|---|---|---| | `SharedPartBuilder` | `.<id>.g.part` → combined `.g.dart` | `part 'x.g.dart';` | yes | | `PartBuilder` | `.<ext>.dart`, e.g. `.freezed.dart` | `part 'x.freezed.dart';` | no | | `LibraryBuilder` | standalone `.dart` library | `import` | no | ## Rules generator authors follow - Use `SharedPartBuilder` for the `.g.dart` convention, and **never** give any other builder the `.g.dart` extension; outputs must be unique, so a `PartBuilder` writing `.g.dart` would collide with the combining builder. - Pick a `partId` unique to your package; source_gen validates its format. - A package that publishes a `build.yaml` should depend on `build_config`. ## Why interviewers ask Knowing this explains everyday puzzles: why one `.g.dart` holds code from two packages, why freezed needs a second `part` line, why `.g.part` files never appear in `lib/`, and where to set a header or lint suppression for all generated code at once. ## Reading a combined file Open any `.g.dart` built by more than one shared-part generator and you will see its structure directly: - the header line, `// GENERATED CODE - DO NOT MODIFY BY HAND`, or your custom `header`; - any `preamble` and the `ignore_for_file` comment built from the combining builder's options; - the `part of` line pointing back at the source library; - one section per generator, each introduced by a comment block naming the generator that produced it (source_gen's `writeDescriptions`, on by default). Those section comments are the quickest way to answer "which package wrote this code?" when a generated member misbehaves. ## Writing your own generator, briefly If a team writes an internal generator, the usual path is a `GeneratorForAnnotation<MyAnnotation>` wrapped in a `SharedPartBuilder` with a unique `partId`, declared in the generator package's `build.yaml` with `build_to: cache` and `applies_builders: ["source_gen|combining_builder"]`. Users then need no new `part` line if they already have `.g.dart`.
- Why does a freezed model with JSON support need two part directives?freezed is a `PartBuilder` with its own `.freezed.dart` extension, while json_serializable is a `SharedPartBuilder` whose code is merged into `.g.dart` by source_gen's combining builder. They are two different part files, so the source declares `part 'x.freezed.dart';` and `part 'x.g.dart';`.
- How would you add // coverage:ignore-file to every generated .g.dart in a package?Configure source_gen's `combining_builder` in your `build.yaml` under `targets: $default: builders:` with a `preamble` option containing the comment. The combining builder writes the preamble after the header in every combined file.
saying these in an interview costs you the question
- Believes each generator writes its own .g.dart file directly into lib
- Thinks .g.part files should be committed alongside the .g.dart output
- Assumes freezed output goes into the shared .g.dart file
- Gives a custom PartBuilder the .g.dart extension
- Thinks a LibraryBuilder output is attached with a part directive