Skip to content

Explore: emitter options defined in TypeSpec (TypeGraph)#11101

Draft
timotheeguerin wants to merge 17 commits into
microsoft:mainfrom
timotheeguerin:emitter-options-typespec
Draft

Explore: emitter options defined in TypeSpec (TypeGraph)#11101
timotheeguerin wants to merge 17 commits into
microsoft:mainfrom
timotheeguerin:emitter-options-typespec

Conversation

@timotheeguerin

Copy link
Copy Markdown
Member

Experimental / RFC. This explores defining emitter options as a TypeSpec file, backed by a new internal TypeGraph concept (a self-contained compilation result so one Program can host several graphs). Opening as a draft to validate the approach.

Concept

An emitter declares its options as a .tsp model (EmitterOptions) exported via package.json exports["./options"].typespec, instead of the current hand-written TS interface + JSONSchemaType schema. This gives a single source of truth that can drive config validation, editor completion, and docs.

To support it, Program gains an internal TypeGraph — each emitter's options file compiles into its own self-contained graph (checker / resolver / source resolution / global namespace).

What's in this PR

  • Stabilize the prototype — restore the incremental program-reuse fast-path that was disabled in createTypeGraph; TypeGraph now owns its sourceFiles/jsSourceFiles.
  • Validator coverage — unions, enums, literals, optional/required, unknown-property rejection, custom scalars (e.g. absolutePath extends string), and Record<>.
  • End-to-end wiringloadEmitter validates user options against the resolved EmitterOptions model and reports invalid-emitter-options diagnostics anchored in tspconfig.yaml.
  • Transitional coexistence — the legacy JSON-schema validator stays authoritative while an emitter still ships one; the TypeSpec-options path is enforced only for emitters with no legacy validator.
  • Tests — 19 validator unit tests + an end-to-end test for a greenfield emitter exporting ./options.

✅ Suites green: compiler 3542, openapi3 2284, json-schema 173.

Key finding

The example option models (openapi3, json-schema) are incomplete vs. the real options (e.g. openapi-versions is missing "3.2.0"). Making them authoritative broke ~590 openapi3 tests — hence the transitional "prefer-legacy" rule. Migrating an emitter requires authoring a complete model and then removing its legacy validator.

Remaining work

1. Complete & migrate the option models (per emitter)

  • Author openapi3 / json-schema EmitterOptions so they are an exact superset of the real options (all enum values incl. "3.2.0", every property, correct optionality), then delete the legacy JSONSchemaType schema + $lib.emitter.options so the TypeSpec path becomes authoritative.
  • Decide how universal options (emitter-output-dir, etc.) are modeled vs. injected.

2. Defaults & coercion

  • The validator currently validates only — documented @defaults are not applied. Decide whether the compiler fills defaults / coerces values into EmitterRef.options, or whether that stays the emitter's responsibility. If the compiler applies them, the resolved options that flow to $onEmit need to carry the defaults.

3. Diagnostic quality

  • invalid-emitter-options is a single generic code carrying the validator message. Consider per-category codes (type-mismatch / unknown-property / invalid-value) and registering them in messages.ts so they get docs URLs and are greppable. Verify YAML target resolution for nested paths (arrays, Record entries, nested models).

4. First-class TypeGraph isolation (deferred from this PR)

  • Today the pattern is "program.checker/resolver/sourceFiles reflect the graph currently being built; the main graph builds last." It works for sequential builds but is implicit. Full isolation means createResolver/createChecker read per-graph source files (they iterate program.sourceFiles in ~9 places) so building an options graph never mutates shared program state. Larger, higher-risk core refactor — worth doing before this is non-experimental.
  • Related: decorator state (program.stateMaps/stateSets) is program-global and shared across graphs; evaluate whether option-graph decorators need per-graph state.

5. Performance

  • A separate graph is compiled per emitter that has migrated (loads intrinsics + std lib each time). Measure overhead with several emitters; consider a shared cached std/intrinsic base across graphs, and confirm the options graph participates correctly in incremental reuse / the language server.

6. Editor & config schema

  • Generate the tspconfig.yaml completion JSON-schema for options.<emitter> from the EmitterOptions model so editor completion reflects the same source of truth (today it comes from the legacy schema).

7. Docs & polish

  • Document the exports["./options"].typespec convention and "defining emitter options in TypeSpec".
  • Resolve TODO-TIM in loadEmitter (reuse the real module-resolution logic to locate the options entrypoint instead of reading manifest.exports directly).
  • Confirm naming: is TypeGraph the term we want for internal/public APIs?

…-to-end

- Restore incremental program reuse fast-path inside createTypeGraph (was
  disabled), and give TypeGraph its own sourceFiles/jsSourceFiles.
- Expand the emitter-options validator: unions, enums, literals,
  optional/required, unknown-property rejection, custom scalars, Record<>.
- Wire validation end-to-end in loadEmitter via validateEmitterOptionsAgainstModel,
  reporting located `invalid-emitter-options` diagnostics in tspconfig.yaml.
- Transitional coexistence: legacy JSON-schema validator stays authoritative
  while present; the TypeSpec-options path is enforced only for emitters with no
  legacy validator.
- Add validator unit tests + an end-to-end test for a greenfield emitter.
@microsoft-github-policy-service microsoft-github-policy-service Bot added compiler:core Issues for @typespec/compiler emitter:json-schema emitter:openapi3 Issues for @typespec/openapi3 emitter labels Jun 26, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jun 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

@typespec/bundler

npm i https://pkg.pr.new/@typespec/bundler@11101

@typespec/compiler

npm i https://pkg.pr.new/@typespec/compiler@11101

@typespec/json-schema

npm i https://pkg.pr.new/@typespec/json-schema@11101

@typespec/openapi3

npm i https://pkg.pr.new/@typespec/openapi3@11101

@typespec/tspd

npm i https://pkg.pr.new/@typespec/tspd@11101

commit: e55446e

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • @typespec/bundler
  • @typespec/compiler
  • @typespec/json-schema
  • @typespec/openapi3
  • @typespec/tspd
Show changes

@typespec/bundler - fix ✏️

Skip exports that only expose a typespec entrypoint (e.g. ./emitter, ./options) when building the JS bundle. These exports have no JS module to bundle and their TypeSpec source files are already included via the sub-export compilation, so the bundler no longer fails with a "missing import or default entrypoint" error.

@typespec/compiler - internal ✏️

Introduce internal TypeGraph concept (a self-contained compilation result) and experimental support for defining emitter options as a TypeSpec file (exports["./options"].typespec). Emitters opt into validating user options against their exported EmitterOptions model via the experimentalEmitterOptions package flag (definePackageFlags).

@typespec/json-schema - internal ✏️

Migrate the JSON Schema emitter to define its options as a TypeSpec model (options/main.tsp, exported via exports["./options"].typespec) instead of a hand-written JSON schema. The compiler now validates user options against that model. Removes the internal EmitterOptionsSchema export.

@typespec/openapi3 - internal ✏️

Migrate the OpenAPI3 emitter to define its options as a TypeSpec model (options/main.tsp, exported via exports["./options"].typespec) instead of a hand-written JSON schema. The compiler now validates user options against that model. Removes the internal EmitterOptionsSchema export.

@typespec/tspd - internal ✏️

Generate emitter options reference docs from an emitter's TypeSpec options/main.tsp model when no legacy JSON-schema validator is present.

Define each emitter's options as a TypeSpec model (options/main.tsp,
exported via exports["./options"].typespec) and remove the legacy
hand-written JSON-schema validators so the compiler validates user
options against the model. Adds end-to-end emitter-options tests.
Exports that only expose a `typespec` condition (e.g. `./emitter`,
`./options`) have no JS module to bundle; their source files are already
included via the sub-export compilation loop. Previously the bundler threw
"missing import or default entrypoint" for these, breaking the playground
bundle.
@azure-sdk-automation

azure-sdk-automation Bot commented Jun 26, 2026

Copy link
Copy Markdown

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

Add model-based emitter-options extraction so tspd reads options/main.tsp when no legacy JSON-schema validator is present, restoring the options reference sections for migrated emitters (openapi3, json-schema).
@microsoft-github-policy-service microsoft-github-policy-service Bot added meta:website TypeSpec.io updates tspd Issues for the tspd tool labels Jun 26, 2026
Gate TypeSpec-model option validation behind a per-emitter experimentalEmitterOptions package flag, broaden validator scalar support, restore absolutePath parity, register diagnostics, scope resolveTypeReference to the graph checker, and generate option TS types/docs from the model.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compiler:core Issues for @typespec/compiler emitter:json-schema emitter:openapi3 Issues for @typespec/openapi3 emitter meta:website TypeSpec.io updates tspd Issues for the tspd tool

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant