Skip to content
Merged
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/compiler"
---

`tsp compile .` now resolves the entrypoint from `exports["."]["typespec"]` in package.json, taking precedence over the legacy `tspMain` field
35 changes: 33 additions & 2 deletions packages/compiler/src/core/entrypoint-resolution.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 2 additions & 3 deletions packages/compiler/src/utils/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,8 @@ export function omitUndefined<T extends Record<string, unknown>>(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.
* Extract package.json's tspMain entry point.
* @param packageJson Parsed package.json object.
*/
export function resolveTspMain(packageJson: any): string | undefined {
if (packageJson?.tspMain !== undefined) {
Expand Down
38 changes: 38 additions & 0 deletions packages/compiler/test/core/entrypoint-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("exports['.']['typespec'] takes precedence over tspMain", 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/other.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"));
});
Loading