From 693cbef7c994ae4ad39d0ac08f7031f501c45e97 Mon Sep 17 00:00:00 2001 From: Michael Nash Date: Wed, 10 Jun 2026 15:22:57 -0700 Subject: [PATCH] feat(compiler): prefer client.tsp over main.tsp when no entrypoint configured When resolving the default entrypoint and no entrypoint is explicitly set (via tspconfig.yaml entrypoint or package.json tspMain), prefer client.tsp over main.tsp when a sibling client.tsp exists. This matches the convention already used by tsp-client and allows augment decorators in client.tsp (e.g. @@clientName) to participate in compilation and linting without requiring imports: - ./client.tsp in tspconfig.yaml. client.tsp is expected to import './main.tsp' per existing convention, which keeps the dependency direction client -> service intact. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...client-tsp-entrypoint-2026-6-10-15-15-0.md | 7 +++++++ .../src/core/entrypoint-resolution.ts | 10 ++++++++-- .../src/server/entrypoint-resolver.ts | 5 +++-- .../test/server/entrypoint-resolver.test.ts | 19 +++++++++++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 .chronus/changes/prefer-client-tsp-entrypoint-2026-6-10-15-15-0.md diff --git a/.chronus/changes/prefer-client-tsp-entrypoint-2026-6-10-15-15-0.md b/.chronus/changes/prefer-client-tsp-entrypoint-2026-6-10-15-15-0.md new file mode 100644 index 00000000000..7ae1c19810f --- /dev/null +++ b/.chronus/changes/prefer-client-tsp-entrypoint-2026-6-10-15-15-0.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Default entrypoint resolution now prefers `client.tsp` over `main.tsp` when a project's `tspconfig.yaml` does not explicitly set `entrypoint` and a sibling `client.tsp` exists. This matches the convention used by `tsp-client` and allows augment decorators in `client.tsp` (e.g. `@@clientName`) to participate in compilation and linting without needing to add `imports: - ./client.tsp` to `tspconfig.yaml`. diff --git a/packages/compiler/src/core/entrypoint-resolution.ts b/packages/compiler/src/core/entrypoint-resolution.ts index e9c2e4ff4c8..0c5256b71f7 100644 --- a/packages/compiler/src/core/entrypoint-resolution.ts +++ b/packages/compiler/src/core/entrypoint-resolution.ts @@ -36,7 +36,7 @@ export async function resolveTypeSpecEntrypointForDir( // Check for project tspconfig first const config = await loadTypeSpecConfigForPath(host, dir, false, false); if (config.kind === "project") { - const entrypoint = config.entrypoint ?? "main.tsp"; + const entrypoint = config.entrypoint ?? (await resolveDefaultEntrypoint(host, dir)); return resolvePath(dir, entrypoint); } @@ -49,5 +49,11 @@ export async function resolveTypeSpecEntrypointForDir( return resolvePath(dir, tspMain); } - return resolvePath(dir, "main.tsp"); + return resolvePath(dir, await resolveDefaultEntrypoint(host, dir)); +} + +async function resolveDefaultEntrypoint(host: CompilerHost, dir: string): Promise { + const clientTspPath = resolvePath(dir, "client.tsp"); + const stat = await doIO(host.stat, clientTspPath, () => {}, { allowFileNotFound: true }); + return stat?.isFile() ? "client.tsp" : "main.tsp"; } diff --git a/packages/compiler/src/server/entrypoint-resolver.ts b/packages/compiler/src/server/entrypoint-resolver.ts index f2ede7c0e40..654fffaad73 100644 --- a/packages/compiler/src/server/entrypoint-resolver.ts +++ b/packages/compiler/src/server/entrypoint-resolver.ts @@ -24,14 +24,15 @@ export async function resolveEntrypointFile( let dir = isFilePath ? getDirectoryPath(path) : path; if (!entrypoints) { - entrypoints = ["main.tsp"]; + entrypoints = ["client.tsp", "main.tsp"]; } while (true) { // Check for project tspconfig first (highest priority) const config = await loadTypeSpecConfigForPath(host, dir, false, false); if (config.kind === "project") { - const entrypoint = config.entrypoint ?? "main.tsp"; + const entrypoint = + config.entrypoint ?? ((await existingFile(dir, "client.tsp")) ? "client.tsp" : "main.tsp"); const candidate = await existingFile(dir, entrypoint); logDebug({ level: "debug", diff --git a/packages/compiler/test/server/entrypoint-resolver.test.ts b/packages/compiler/test/server/entrypoint-resolver.test.ts index 30fefa495e6..52346eb22a5 100644 --- a/packages/compiler/test/server/entrypoint-resolver.test.ts +++ b/packages/compiler/test/server/entrypoint-resolver.test.ts @@ -70,6 +70,13 @@ describe("entrypoint resolution", () => { const resultForUndefined = await resolveEntrypoint(files, "project/src/file.tsp", undefined); expect(resultForUndefined).toBe(resolveVirtualPath("project/main.tsp")); }); + + it("prefers client.tsp over main.tsp in default fallback when both exist", async () => { + const files = { "project/main.tsp": "", "project/client.tsp": "" }; + + const result = await resolveEntrypoint(files, "project/src/file.tsp"); + expect(result).toBe(resolveVirtualPath("project/client.tsp")); + }); }); describe("project tspconfig entrypoint resolution", () => { @@ -106,6 +113,18 @@ describe("project tspconfig entrypoint resolution", () => { expect(result).toBe(resolveVirtualPath("project/main.tsp")); }); + it("defaults to client.tsp when kind is project, no entrypoint specified, and client.tsp exists", async () => { + const result = await resolveEntrypoint( + { + "project/tspconfig.yaml": "kind: project\n", + "project/main.tsp": "", + "project/client.tsp": "", + }, + "project/src/doc.tsp", + ); + expect(result).toBe(resolveVirtualPath("project/client.tsp")); + }); + it("project tspconfig stops the walk even if entrypoint file doesn't exist", async () => { const result = await resolveEntrypoint( {