Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/bundler"
---

Include a library's own `tspconfig.yaml` in the generated bundle so per-library opt-ins (such as compiler `features`) are preserved when the library is loaded in the browser (e.g. in the playground).
20 changes: 20 additions & 0 deletions .chronus/changes/on-info-hook-2026-7-30-12-30-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Add a new experimental `$onInfo` library hook and `program.getTypeInfo(type)` API allowing libraries to contribute extra, domain-specific information about types. Unlike `$onValidate`, this hook never runs during compilation and must not mutate the type graph — it is invoked lazily and on demand (e.g. by the language server for hover documentation, or by tooling querying the type).

The hook is gated by the `type-info-hook` compiler feature, scoped to the package that declares it: a library opts in via its own `tspconfig.yaml` and consumers do not need to enable anything.

```ts
// A library exports a provider (use `defineInfoHook` for typing):
export const $onInfo = defineInfoHook(({ program, target }) => {
if (target.kind !== "Operation") return undefined;
return { content: "extra info about this operation" };
});

// Tooling / language server queries it (merges every library's contribution):
const info = program.getTypeInfo(type);
```
12 changes: 12 additions & 0 deletions .chronus/changes/on-info-http-route-2026-7-30-12-30-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
changeKind: feature
packages:
- "@typespec/http"
---

Add an `$onInfo` provider that surfaces the resolved HTTP route (verb and URI template) and response status codes of an operation. This is shown when hovering an operation in the IDE and can be queried programmatically via `program.getTypeInfo(operation)`.

```ts
const info = program.getTypeInfo(operation);
// { content: "`HTTP Route`: `GET /pets/{id}`\n\n`Responses`: `204`" }
```
19 changes: 19 additions & 0 deletions packages/bundler/src/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,13 @@ async function createEsBuildContext(
[normalizePath(join(libraryPath, "package.json"))]: JSON.stringify(definition.packageJson),
};

// Include the library's own `tspconfig.yaml` so per-library opt-ins (e.g. compiler
// `features`) are preserved in the bundle.
const tspconfig = await tryReadFile(join(libraryPath, "tspconfig.yaml"));
if (tspconfig !== undefined) {
typespecFiles[normalizePath(join(libraryPath, "tspconfig.yaml"))] = tspconfig;
}

for (const [filename, sourceFile] of program.sourceFiles) {
typespecFiles[filename] = sourceFile.file.text;
}
Expand Down Expand Up @@ -339,6 +346,18 @@ async function readLibraryPackageJson(path: string): Promise<PackageJson> {
return JSON.parse(file.toString());
}

/** Read a file, returning `undefined` when it does not exist. */
async function tryReadFile(path: string): Promise<string | undefined> {
try {
return (await readFile(path)).toString();
} catch (error: any) {
if (error.code === "ENOENT") {
return undefined;
}
throw error;
}
}

/**
* Resolve which peer dependencies should be treated as external.
* Only peer dependencies that are TypeSpec libraries (have `tspMain` in their package.json)
Expand Down
14 changes: 14 additions & 0 deletions packages/compiler/src/core/binder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mutate } from "../utils/misc.js";
import { compilerAssert } from "./diagnostics.js";
import { isCompilerFeatureEnabled } from "./features.js";
import { getLocationContext } from "./helpers/location-context.js";
import { visitChildren } from "./parser.js";
import type { Program } from "./program.js";
Expand Down Expand Up @@ -179,6 +180,19 @@ export function createBinder(program: Program): Binder {
: ({ type: "file" } satisfies FileLibraryMetadata);
program.onValidate(member as any, metadata);
continue;
} else if (name === "onInfo") {
// `$onInfo` is experimental: only respect it when the library (or project) that
// declares the hook opted into the `type-info-hook` feature in its own
// `tspconfig.yaml`. Consumers do not need to enable anything.
if (isCompilerFeatureEnabled(program, "type-info-hook", sourceFile)) {
const context = getLocationContext(program, sourceFile);
const metadata =
context.type === "library"
? context.metadata
: ({ type: "file" } satisfies FileLibraryMetadata);
program.registerInfoProvider(member as any, metadata);
}
continue;
} else if (name === "onEmit") {
// nothing to do here this is loaded as emitter.
continue;
Expand Down
6 changes: 4 additions & 2 deletions packages/compiler/src/core/external-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type Colors =
| "bgWhite";

export interface ExternalErrorInfo {
kind: "emitter" | "validator";
kind: "emitter" | "validator" | "info";
error: unknown;
metadata: LibraryMetadata;
}
Expand All @@ -56,7 +56,9 @@ function renderExternalErrorInfo(
color(
kind === "emitter"
? `Emitter "${metadata.name}" crashed! This is a bug.`
: `Library "${metadata.name}" $onValidate crashed! This is a bug.`,
: kind === "info"
? `Library "${metadata.name}" $onInfo crashed! This is a bug.`
: `Library "${metadata.name}" $onValidate crashed! This is a bug.`,
"red",
),
];
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/core/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export const compilerFeatures = {
description:
"Allows use of auto decorator declarations without experimental warnings in project code.",
},
"type-info-hook": {
description:
"Enables the experimental `$onInfo` hook allowing libraries to contribute extra information about types to IDE hover and tooling (queried via `program.getTypeInfo`).",
},
} as const satisfies Record<string, CompilerFeatureDefinition>;

export type CompilerFeatureName = keyof typeof compilerFeatures;
Expand Down
18 changes: 18 additions & 0 deletions packages/compiler/src/core/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
JSONSchemaValidator,
LinterDefinition,
LinterRuleDefinition,
OnInfoHook,
PackageFlags,
StateDef,
TypeSpecLibrary,
Expand Down Expand Up @@ -114,6 +115,23 @@ export function defineLinter(def: LinterDefinition): LinterDefinition {
return def;
}

/**
* Define a `$onInfo` hook that contributes extra information about types to IDE hover
* documentation and tooling. This helper only provides typing; export the result as `$onInfo`
* from your library's entry point.
*
* @example
* ```ts
* export const $onInfo = defineInfoHook(({ target }) => {
* if (target.kind !== "Operation") return undefined;
* return { content: `Operation ${target.name}` };
* });
* ```
*/
export function defineInfoHook(hook: OnInfoHook): OnInfoHook {
return hook;
}

/** Create a new linter rule. */
export function createLinterRule<
const N extends string,
Expand Down
58 changes: 58 additions & 0 deletions packages/compiler/src/core/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import {
EmitContext,
EmitterFunc,
Entity,
InfoContext,
JsSourceFileNode,
LibraryInstance,
LibraryMetadata,
Expand All @@ -70,6 +71,7 @@ import {
Namespace,
NoTarget,
Node,
OnInfoHook,
PerfReporter,
SourceFile,
Sym,
Expand All @@ -78,6 +80,7 @@ import {
TemplateInstanceTarget,
Tracer,
Type,
TypeInfo,
TypeSpecLibrary,
TypeSpecScriptNode,
} from "./types.js";
Expand Down Expand Up @@ -118,6 +121,22 @@ export interface Program {
cb: (program: Program) => void | Promise<void>,
LibraryMetadata: LibraryMetadata,
): void;
/**
* Register a provider that contributes extra information about types. Libraries wire this up
* by exporting a `$onInfo` hook; it is discovered automatically by the compiler.
* @internal
*/
registerInfoProvider(provider: OnInfoHook, metadata: LibraryMetadata): void;
/**
* Query the registered `$onInfo` providers for extra information about the given type. Returns
* the merged information contributed by every library, or `undefined` when none contributed.
* Providers are run lazily and never mutate the type graph. Used by the language server for
* hover docs and by tooling.
*
* Only libraries that opted into the experimental `type-info-hook` feature in their own
* `tspconfig.yaml` contribute; consumers do not need to enable anything.
*/
getTypeInfo(target: Type): TypeInfo | undefined;
/** @internal */
getOption(key: string): string | undefined;
stateSet(key: symbol): Set<Type>;
Expand Down Expand Up @@ -177,6 +196,11 @@ interface Validator {
) => void | readonly Diagnostic[] | Promise<void> | Promise<readonly Diagnostic[]>;
}

interface InfoProvider {
metadata: LibraryMetadata;
callback: OnInfoHook;
}

interface TypeSpecLibraryReference {
path: string;
manifest: PackageJson;
Expand Down Expand Up @@ -238,6 +262,7 @@ async function createProgram(
): Promise<{ program: Program; shouldAbort: boolean }> {
const runtimeStats: Partial<RuntimeStats> = {};
const validateCbs: Validator[] = [];
const infoProviders: InfoProvider[] = [];
const stateMaps = new Map<symbol, Map<Type, unknown>>();
const stateSets = new Map<symbol, Set<Type>>();
const diagnostics: Diagnostic[] = [];
Expand Down Expand Up @@ -301,6 +326,39 @@ async function createProgram(
onValidate(cb, metadata) {
validateCbs.push({ callback: cb, metadata });
},
registerInfoProvider(provider, metadata) {
infoProviders.push({ callback: provider, metadata });
},
getTypeInfo(target) {
if (infoProviders.length === 0) {
return undefined;
}
const contents: string[] = [];
const context: InfoContext = { program, target };
for (const provider of infoProviders) {
let result: TypeInfo | undefined;
try {
result = provider.callback(context);
} catch (error: any) {
// This runs lazily, long after compilation finished, so a crashing provider must not
// mutate the program's diagnostics. In a design time build (language server) trace it
// and carry on so a broken library cannot take down hover or completion.
if (options.designTimeBuild) {
trace(
"info-provider.crash",
`Library "${provider.metadata.name ?? "<unnamed>"}" $onInfo crashed: ${error.stack}`,
);
continue;
} else {
throw new ExternalError({ kind: "info", metadata: provider.metadata, error });
}
}
if (result) {
contents.push(result.content);
}
}
return contents.length > 0 ? { content: contents.join("\n\n") } : undefined;
},
getGlobalNamespaceType,
resolveTypeReference,
/** @internal */
Expand Down
31 changes: 31 additions & 0 deletions packages/compiler/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2351,6 +2351,37 @@ export type SemanticNodeListener = {
} & TypeListeners &
ValueListeners;

/**
* Extra information about a type contributed by a library through the `$onInfo` hook. Used to
* enrich IDE hover documentation and to answer queries (e.g. from AI agents/tooling).
*/
export interface TypeInfo {
/** Markdown content describing this information, e.g. ``"`HTTP Route`: `GET /pets/{id}`"``. */
readonly content: string;
}

/**
* Context passed to a library's `$onInfo` hook. Additional properties may be added over time.
*/
export interface InfoContext {
/** The current program. */
readonly program: Program;
/** The type the information is being requested for. */
readonly target: Type;
}

/**
* Library hook returning extra information about a given type.
*
* A library provides this by exporting a `$onInfo` function (typically via {@link defineInfoHook}).
* Unlike `$onValidate`, this hook is never run during compilation and must not mutate the type
* graph. It is invoked lazily and on demand (e.g. by the language server when computing hover
* documentation, or by tooling querying {@link Program.getTypeInfo}).
*
* This hook is gated behind the experimental `type-info-hook` compiler feature.
*/
export type OnInfoHook = (context: InfoContext) => TypeInfo | undefined;

export type DiagnosticReportWithoutTarget<
T extends { [code: string]: DiagnosticMessages },
C extends keyof T,
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export {
export {
createLinterRule as createRule,
createTypeSpecLibrary,
defineInfoHook,
defineLinter,
definePackageFlags,
paramMessage,
Expand Down Expand Up @@ -379,6 +380,7 @@ export type {
IdentifierContext,
IdentifierKind,
IndeterminateEntity,
InfoContext,
InsertTextCodeFixEdit,
Interface,
IntrinsicScalarName,
Expand Down Expand Up @@ -425,6 +427,7 @@ export type {
NumericValue,
ObjectValue,
ObjectValuePropertyDescriptor,
OnInfoHook,
Operation,
OperationSignature,
PackageFlags,
Expand Down Expand Up @@ -468,6 +471,7 @@ export type {
TracerOptions,
Tuple,
Type,
TypeInfo,
TypeInstantiationMap,
TypeListeners,
TypeMapper,
Expand Down
28 changes: 28 additions & 0 deletions packages/compiler/src/server/type-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,37 @@ export async function getSymbolDetails(
);
}

const infoType = resolveSymbolType(program, symbol);
if (infoType) {
const info = program.getTypeInfo(infoType);
if (info) {
// Separate library contributed info from the type's own signature/documentation with a
// horizontal rule so it is clearly not part of the doc comment.
if (lines.length > 0) {
lines.push("---");
}
lines.push(info.content);
}
}

return lines.join("\n\n");
}

/** Resolve the {@link Type} a symbol refers to, if any. */
function resolveSymbolType(program: Program, symbol: Sym): Type | undefined {
if (symbol.type) {
return symbol.type;
}
const symNode = getSymNode(symbol);
if (symNode) {
const entity = program.checker.getTypeOrValueForNode(symNode);
if (entity && isType(entity)) {
return entity;
}
}
return undefined;
}

function getSymbolDocumentation(program: Program, symbol: Sym) {
const docs: string[] = [];

Expand Down
1 change: 1 addition & 0 deletions packages/compiler/test/core/cli/actions/info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ it("lists available compiler features and marks enabled features", () => {
"",
" enabled function-declarations Allows use of function declarations without experimental warnings in project code.",
" disabled auto-decorators Allows use of auto decorator declarations without experimental warnings in project code.",
" disabled type-info-hook Enables the experimental `$onInfo` hook allowing libraries to contribute extra information about types to IDE hover and tooling (queried via `program.getTypeInfo`).",
]);
});
Loading
Loading