diff --git a/packages/emitter/lib/decorators.tsp b/packages/emitter/lib/decorators.tsp index 7034f4d..8354b5a 100644 --- a/packages/emitter/lib/decorators.tsp +++ b/packages/emitter/lib/decorators.tsp @@ -1,5 +1,7 @@ import "../dist/tsp-index.js"; +using TypeSpec.Reflection; + /** * Specifies the maximum number of graphemes (user-perceived characters) allowed. * Used alongside maxLength for proper Unicode text handling. @@ -159,3 +161,16 @@ extern dec encoding(target: unknown, mime: valueof string); * ``` */ extern dec errors(target: unknown, ...errors: unknown[]); + +/** + * Marks a namespace as external, preventing it from emitting JSON output. + * This decorator can only be applied to namespaces. + * Useful for importing definitions from other lexicons without re-emitting them. + * + * @example + * ```typespec + * @external + * namespace com.atproto.repo.defs; + * ``` + */ +extern dec external(target: Namespace); diff --git a/packages/emitter/src/decorators.ts b/packages/emitter/src/decorators.ts index c86d100..41aa54a 100644 --- a/packages/emitter/src/decorators.ts +++ b/packages/emitter/src/decorators.ts @@ -24,6 +24,7 @@ const encodingKey = Symbol("encoding"); const inlineKey = Symbol("inline"); const maxBytesKey = Symbol("maxBytes"); const minBytesKey = Symbol("minBytes"); +const externalKey = Symbol("external"); /** * @maxBytes decorator for maximum length of bytes type @@ -294,3 +295,24 @@ export function $readOnly(context: DecoratorContext, target: Type) { export function isReadOnly(program: Program, target: Type): boolean { return program.stateSet(readOnlyKey).has(target); } + +/** + * @external decorator for marking a namespace as external + * External namespaces are skipped during emission and don't produce JSON files + */ +export function $external(context: DecoratorContext, target: Type) { + if (target.kind !== "Namespace") { + context.program.reportDiagnostic({ + code: "external-not-on-namespace", + severity: "error", + message: "@external decorator can only be applied to namespaces", + target: target, + }); + return; + } + context.program.stateSet(externalKey).add(target); +} + +export function isExternal(program: Program, target: Type): boolean { + return program.stateSet(externalKey).has(target); +} diff --git a/packages/emitter/src/emitter.ts b/packages/emitter/src/emitter.ts index 0f4fa93..c7ed03e 100644 --- a/packages/emitter/src/emitter.ts +++ b/packages/emitter/src/emitter.ts @@ -67,6 +67,7 @@ import { isInline, getMaxBytes, getMinBytes, + isExternal, } from "./decorators.js"; export interface EmitterOptions { @@ -121,6 +122,11 @@ export class TypelexEmitter { return; } + // Skip external namespaces - they don't emit JSON files + if (isExternal(this.program, ns)) { + return; + } + // Check for TypeSpec enum syntax and throw error if (ns.enums && ns.enums.size > 0) { for (const [_, enumType] of ns.enums) { diff --git a/packages/emitter/src/tsp-index.ts b/packages/emitter/src/tsp-index.ts index 5d93ede..f20114c 100644 --- a/packages/emitter/src/tsp-index.ts +++ b/packages/emitter/src/tsp-index.ts @@ -14,6 +14,7 @@ import { $inline, $maxBytes, $minBytes, + $external, } from "./decorators.js"; /** @internal */ @@ -34,5 +35,6 @@ export const $decorators = { inline: $inline, maxBytes: $maxBytes, minBytes: $minBytes, + external: $external, }, };