From 59128e5858e2a11f047b5e418fdde3338a22ad6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:48:24 +0000 Subject: [PATCH 1/5] Initial plan From 1ce2f0ae8c780a5a9b2f13d41d6f2c774f6cc591 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:56:39 +0000 Subject: [PATCH 2/5] fix(compiler): resolve entrypoint from exports['.']['typespec'] in package.json Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/src/utils/misc.ts | 15 +++++++- .../test/core/entrypoint-resolution.test.ts | 38 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/utils/misc.ts b/packages/compiler/src/utils/misc.ts index eb4aebb7ba9..bf1c4f6e457 100644 --- a/packages/compiler/src/utils/misc.ts +++ b/packages/compiler/src/utils/misc.ts @@ -220,13 +220,24 @@ export function omitUndefined>(data: T): T { /** * Extract package.json's tspMain entry point in a given path. - * @param path Path that contains package.json - * @param reportDiagnostic optional diagnostic handler. + * Checks `tspMain` field first, then falls back to `exports["."]["typespec"]`. + * @param packageJson Parsed package.json object. */ export function resolveTspMain(packageJson: any): string | undefined { if (packageJson?.tspMain !== undefined) { return packageJson.tspMain; } + // Fall back to `exports["."]["typespec"]` condition + const exports = packageJson?.exports; + if (exports !== undefined && exports !== null && typeof exports === "object") { + const rootExport = exports["."]; + if (rootExport !== undefined && rootExport !== null && typeof rootExport === "object") { + const typespecCondition = rootExport["typespec"]; + if (typeof typespecCondition === "string") { + return typespecCondition; + } + } + } return undefined; } diff --git a/packages/compiler/test/core/entrypoint-resolution.test.ts b/packages/compiler/test/core/entrypoint-resolution.test.ts index 79f11c42882..db74acc382c 100644 --- a/packages/compiler/test/core/entrypoint-resolution.test.ts +++ b/packages/compiler/test/core/entrypoint-resolution.test.ts @@ -43,3 +43,41 @@ it("defaults to main.tsp when neither tspconfig entrypoint nor package.json tspM const entrypoint = await resolveTypeSpecEntrypointForDir(host, dir, () => {}); expect(entrypoint).toBe(resolveVirtualPath("my-lib/main.tsp")); }); + +it("uses package.json exports['.']['typespec'] when there is no tspMain", async () => { + fs.add( + "my-lib/package.json", + JSON.stringify({ + name: "my-lib", + exports: { ".": { typespec: "./lib/main.tsp", default: "./dist/index.js" } }, + }), + ); + const entrypoint = await resolveTypeSpecEntrypointForDir(host, dir, () => {}); + expect(entrypoint).toBe(resolveVirtualPath("my-lib/lib/main.tsp")); +}); + +it("tspMain takes precedence over exports['.']['typespec']", async () => { + fs.add( + "my-lib/package.json", + JSON.stringify({ + name: "my-lib", + tspMain: "lib/main.tsp", + exports: { ".": { typespec: "./lib/other.tsp", default: "./dist/index.js" } }, + }), + ); + const entrypoint = await resolveTypeSpecEntrypointForDir(host, dir, () => {}); + expect(entrypoint).toBe(resolveVirtualPath("my-lib/lib/main.tsp")); +}); + +it("an explicit entrypoint in tspconfig.yaml takes precedence over exports['.']['typespec']", async () => { + fs.add( + "my-lib/package.json", + JSON.stringify({ + name: "my-lib", + exports: { ".": { typespec: "./lib/main.tsp", default: "./dist/index.js" } }, + }), + ); + fs.add("my-lib/tspconfig.yaml", `kind: project\nentrypoint: other.tsp`); + const entrypoint = await resolveTypeSpecEntrypointForDir(host, dir, () => {}); + expect(entrypoint).toBe(resolveVirtualPath("my-lib/other.tsp")); +}); From a5cfa20871be593776108c31f599ac9638b2f9f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:57:02 +0000 Subject: [PATCH 3/5] fix(compiler): resolve entrypoint from exports['.']['typespec'] in package.json Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../copilot-add-support-for-exports-2026-6-28-13-56-41.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/copilot-add-support-for-exports-2026-6-28-13-56-41.md diff --git a/.chronus/changes/copilot-add-support-for-exports-2026-6-28-13-56-41.md b/.chronus/changes/copilot-add-support-for-exports-2026-6-28-13-56-41.md new file mode 100644 index 00000000000..370feb5da06 --- /dev/null +++ b/.chronus/changes/copilot-add-support-for-exports-2026-6-28-13-56-41.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +`tsp compile .` now resolves the entrypoint from `exports["."]["typespec"]` in package.json when `tspMain` is not set \ No newline at end of file From eda023554df8cc0b2c3223803ec1f46afbdbe1e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:22:30 +0000 Subject: [PATCH 4/5] fix(compiler): exports['.']['typespec'] takes precedence over tspMain in entrypoint resolution" Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...ilot-add-support-for-exports-2026-6-28-13-56-41.md | 2 +- packages/compiler/src/utils/misc.ts | 11 ++++++----- .../compiler/test/core/entrypoint-resolution.test.ts | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.chronus/changes/copilot-add-support-for-exports-2026-6-28-13-56-41.md b/.chronus/changes/copilot-add-support-for-exports-2026-6-28-13-56-41.md index 370feb5da06..fddb2fb1e9d 100644 --- a/.chronus/changes/copilot-add-support-for-exports-2026-6-28-13-56-41.md +++ b/.chronus/changes/copilot-add-support-for-exports-2026-6-28-13-56-41.md @@ -4,4 +4,4 @@ packages: - "@typespec/compiler" --- -`tsp compile .` now resolves the entrypoint from `exports["."]["typespec"]` in package.json when `tspMain` is not set \ No newline at end of file +`tsp compile .` now resolves the entrypoint from `exports["."]["typespec"]` in package.json, taking precedence over the legacy `tspMain` field \ No newline at end of file diff --git a/packages/compiler/src/utils/misc.ts b/packages/compiler/src/utils/misc.ts index bf1c4f6e457..5e556e55d2e 100644 --- a/packages/compiler/src/utils/misc.ts +++ b/packages/compiler/src/utils/misc.ts @@ -220,14 +220,11 @@ export function omitUndefined>(data: T): T { /** * Extract package.json's tspMain entry point in a given path. - * Checks `tspMain` field first, then falls back to `exports["."]["typespec"]`. + * Checks `exports["."]["typespec"]` first, then falls back to `tspMain`. * @param packageJson Parsed package.json object. */ export function resolveTspMain(packageJson: any): string | undefined { - if (packageJson?.tspMain !== undefined) { - return packageJson.tspMain; - } - // Fall back to `exports["."]["typespec"]` condition + // Prefer `exports["."]["typespec"]` condition, consistent with how module resolution works. const exports = packageJson?.exports; if (exports !== undefined && exports !== null && typeof exports === "object") { const rootExport = exports["."]; @@ -238,6 +235,10 @@ export function resolveTspMain(packageJson: any): string | undefined { } } } + // Fall back to legacy `tspMain` field. + if (packageJson?.tspMain !== undefined) { + return packageJson.tspMain; + } return undefined; } diff --git a/packages/compiler/test/core/entrypoint-resolution.test.ts b/packages/compiler/test/core/entrypoint-resolution.test.ts index db74acc382c..be6d8686e36 100644 --- a/packages/compiler/test/core/entrypoint-resolution.test.ts +++ b/packages/compiler/test/core/entrypoint-resolution.test.ts @@ -56,7 +56,7 @@ it("uses package.json exports['.']['typespec'] when there is no tspMain", async expect(entrypoint).toBe(resolveVirtualPath("my-lib/lib/main.tsp")); }); -it("tspMain takes precedence over exports['.']['typespec']", async () => { +it("exports['.']['typespec'] takes precedence over tspMain", async () => { fs.add( "my-lib/package.json", JSON.stringify({ @@ -66,7 +66,7 @@ it("tspMain takes precedence over exports['.']['typespec']", async () => { }), ); const entrypoint = await resolveTypeSpecEntrypointForDir(host, dir, () => {}); - expect(entrypoint).toBe(resolveVirtualPath("my-lib/lib/main.tsp")); + expect(entrypoint).toBe(resolveVirtualPath("my-lib/lib/other.tsp")); }); it("an explicit entrypoint in tspconfig.yaml takes precedence over exports['.']['typespec']", async () => { From d17b80636bc076393d3d3ef04f319b9899249407 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:35:16 +0000 Subject: [PATCH 5/5] refactor(compiler): use resolvePackageExports for entrypoint exports resolution Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../src/core/entrypoint-resolution.ts | 35 +++++++++++++++++-- packages/compiler/src/utils/misc.ts | 15 +------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/packages/compiler/src/core/entrypoint-resolution.ts b/packages/compiler/src/core/entrypoint-resolution.ts index 6c50cc96a47..8e69cf3d9e9 100644 --- a/packages/compiler/src/core/entrypoint-resolution.ts +++ b/packages/compiler/src/core/entrypoint-resolution.ts @@ -1,4 +1,7 @@ import { loadTypeSpecConfigForPath } from "../config/config-loader.js"; +import { resolvePackageExports } from "../module-resolver/esm/resolve-package-exports.js"; +import { NoMatchingConditionsError } from "../module-resolver/esm/utils.js"; +import { fileURLToPath, pathToFileURL } from "../module-resolver/utils.js"; import { doIO, loadFile } from "../utils/io.js"; import { resolveTspMain } from "../utils/misc.js"; import { DiagnosticHandler } from "./diagnostics.js"; @@ -39,12 +42,40 @@ export async function resolveTypeSpecEntrypointForDir( return resolvePath(dir, config.entrypoint); } - // Otherwise fall back to the `tspMain` declared in package.json. A `tspconfig.yaml` - // without an explicit `entrypoint` must not override `tspMain`. const pkgJsonPath = resolvePath(dir, "package.json"); const [pkg] = await loadFile(host, pkgJsonPath, JSON.parse, reportDiagnostic, { allowFileNotFound: true, }); + + // Try exports["."]["typespec"] first using the existing ESM package exports resolver. + if (pkg?.exports) { + try { + const match = await resolvePackageExports( + { + packageUrl: pathToFileURL(dir), + specifier: ".", + moduleDirs: ["node_modules"], + conditions: ["typespec"], + ignoreDefaultCondition: true, + resolveId: () => { + throw new Error("not supported"); + }, + }, + ".", + pkg.exports, + ); + if (match) { + return fileURLToPath(match); + } + } catch (e) { + if (!(e instanceof NoMatchingConditionsError)) { + throw e; + } + // No matching typespec condition — fall through to tspMain / main.tsp + } + } + + // Fall back to the legacy `tspMain` declared in package.json. const tspMain = resolveTspMain(pkg); if (tspMain !== undefined) { return resolvePath(dir, tspMain); diff --git a/packages/compiler/src/utils/misc.ts b/packages/compiler/src/utils/misc.ts index 5e556e55d2e..49b1e7879a9 100644 --- a/packages/compiler/src/utils/misc.ts +++ b/packages/compiler/src/utils/misc.ts @@ -219,23 +219,10 @@ export function omitUndefined>(data: T): T { } /** - * Extract package.json's tspMain entry point in a given path. - * Checks `exports["."]["typespec"]` first, then falls back to `tspMain`. + * Extract package.json's tspMain entry point. * @param packageJson Parsed package.json object. */ export function resolveTspMain(packageJson: any): string | undefined { - // Prefer `exports["."]["typespec"]` condition, consistent with how module resolution works. - const exports = packageJson?.exports; - if (exports !== undefined && exports !== null && typeof exports === "object") { - const rootExport = exports["."]; - if (rootExport !== undefined && rootExport !== null && typeof rootExport === "object") { - const typespecCondition = rootExport["typespec"]; - if (typeof typespecCondition === "string") { - return typespecCondition; - } - } - } - // Fall back to legacy `tspMain` field. if (packageJson?.tspMain !== undefined) { return packageJson.tspMain; }