diff --git a/.chronus/changes/copilot-fix-emitter-not-found-error-message-2026-7-30-16-11-58.md b/.chronus/changes/copilot-fix-emitter-not-found-error-message-2026-7-30-16-11-58.md new file mode 100644 index 00000000000..250210b31e7 --- /dev/null +++ b/.chronus/changes/copilot-fix-emitter-not-found-error-message-2026-7-30-16-11-58.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Report better error message when specifying an emitter that is not installed with `--emit` flag diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 5e5f832eca3..c91a2528634 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -805,6 +805,12 @@ const diagnostics = { default: paramMessage`onValidate failed with errors. ${"error"}`, }, }, + "emitter-not-found": { + severity: "error", + messages: { + default: paramMessage`Emitter "${"emitterPackage"}" not found. Make sure to install it with \`npm install ${"emitterPackage"}\`.`, + }, + }, "invalid-emitter": { severity: "error", messages: { diff --git a/packages/compiler/src/core/program.ts b/packages/compiler/src/core/program.ts index 2fb80dad111..b5608ea673e 100644 --- a/packages/compiler/src/core/program.ts +++ b/packages/compiler/src/core/program.ts @@ -578,6 +578,7 @@ async function createProgram( async function loadLibrary( basedir: string, libraryNameOrPath: string, + context?: { isEmitter?: boolean }, ): Promise { const [resolution, diagnostics] = await resolveEmitterModuleAndEntrypoint( basedir, @@ -585,7 +586,17 @@ async function createProgram( ); if (resolution === undefined) { - program.reportDiagnostics(diagnostics); + if (context?.isEmitter && diagnostics.some((d) => d.code === "import-not-found")) { + program.reportDiagnostic( + createDiagnostic({ + code: "emitter-not-found", + format: { emitterPackage: libraryNameOrPath }, + target: NoTarget, + }), + ); + } else { + program.reportDiagnostics(diagnostics); + } return undefined; } const { module, entrypoint } = resolution; @@ -606,7 +617,7 @@ async function createProgram( emitterNameOrPath: string, emittersOptions: Record, ): Promise { - const library = await loadLibrary(basedir, emitterNameOrPath); + const library = await loadLibrary(basedir, emitterNameOrPath, { isEmitter: true }); if (library === undefined) { return undefined; diff --git a/packages/compiler/test/core/emitter.test.ts b/packages/compiler/test/core/emitter.test.ts index b71f110fc87..b4a7d5f43e8 100644 --- a/packages/compiler/test/core/emitter.test.ts +++ b/packages/compiler/test/core/emitter.test.ts @@ -7,7 +7,7 @@ import type { TypeSpecLibraryDef, } from "../../src/index.js"; import { createTypeSpecLibrary } from "../../src/index.js"; -import { expectDiagnosticEmpty } from "../../src/testing/expect.js"; +import { expectDiagnosticEmpty, expectDiagnostics } from "../../src/testing/expect.js"; import { mockFile } from "../../src/testing/fs.js"; import { Tester } from "../tester.js"; @@ -110,3 +110,16 @@ it("when using dry-run only call emitter with the capabilities", async () => { expect(emitter2.$onEmit).not.toHaveBeenCalled(); expect(emitter3.$onEmit).not.toHaveBeenCalled(); }); + +it("reports emitter-not-found when emitter package is not installed", async () => { + const diagnostics = await Tester.diagnose("model Foo {}", { + compilerOptions: { + emit: ["not-installed-emitter"], + }, + }); + + expectDiagnostics(diagnostics, { + code: "emitter-not-found", + message: `Emitter "not-installed-emitter" not found. Make sure to install it with \`npm install not-installed-emitter\`.`, + }); +}); diff --git a/packages/playground/test/deferred-emitter-compile.test.ts b/packages/playground/test/deferred-emitter-compile.test.ts index 30bd4462e11..211fac1f909 100644 --- a/packages/playground/test/deferred-emitter-compile.test.ts +++ b/packages/playground/test/deferred-emitter-compile.test.ts @@ -106,7 +106,7 @@ describe("compiling with a deferred emitter", () => { emit: [emitterName], outputDir: resolveVirtualPath("tsp-output"), }); - expect(before.diagnostics.map((x) => x.code)).toContain("import-not-found"); + expect(before.diagnostics.map((x) => x.code)).toContain("emitter-not-found"); expect(emitted).toBe(0); await host.loadLibrary(emitterName);