Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@azure-tools/typespec-ts"
---

Fix multi-client package build failures by syncing the generated `config/tsconfig.src.*.json` `include` lists with the `warp.config.yml` exports, so every client entry point is compiled and emitted to `dist` (previously warp failed with `DIST_MISSING`).
18 changes: 14 additions & 4 deletions packages/typespec-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,12 +461,12 @@ export async function $onEmit(context: EmitContext) {
// Generate warp.config.yml for Azure monorepo ESM packages
commonBuilders.push((model) => buildWarpConfig(model, modularPackageInfo));
commonBuilders.push(buildTsConfig);
commonBuilders.push(buildTsSrcEsmConfig);
commonBuilders.push(buildTsSrcBrowserConfig);
commonBuilders.push(() => buildTsSrcEsmConfig(modularPackageInfo["exports"]));
commonBuilders.push(() => buildTsSrcBrowserConfig(modularPackageInfo["exports"]));
if (option.generateReactNativeTarget) {
commonBuilders.push(buildTsSrcReactNativeConfig);
commonBuilders.push(() => buildTsSrcReactNativeConfig(modularPackageInfo["exports"]));
}
commonBuilders.push(buildTsSrcCjsConfig);
commonBuilders.push(() => buildTsSrcCjsConfig(modularPackageInfo["exports"]));
if (option.generateSample) {
commonBuilders.push(buildTsSampleConfig);
}
Expand Down Expand Up @@ -531,6 +531,16 @@ export async function $onEmit(context: EmitContext) {
// Update warp.config.yml for Azure monorepo packages
updateBuilders.push((model: ClientModel) => buildWarpConfig(model, modularPackageInfo));

// Keep the tsconfig.src.*.json `include` lists in sync with the warp
// exports so every declared client entry point is compiled and emitted
// to `dist` (otherwise warp fails the build with DIST_MISSING).
Comment thread
kazrael2119 marked this conversation as resolved.
Outdated
updateBuilders.push(() => buildTsSrcEsmConfig(modularPackageInfo["exports"]));
updateBuilders.push(() => buildTsSrcBrowserConfig(modularPackageInfo["exports"]));
if (option.generateReactNativeTarget) {
updateBuilders.push(() => buildTsSrcReactNativeConfig(modularPackageInfo["exports"]));
}
updateBuilders.push(() => buildTsSrcCjsConfig(modularPackageInfo["exports"]));

// If the client name changed, regenerate the README and snippets completely;
// otherwise update only the API reference link in-place.
if (hasReadmeFile) {
Expand Down
44 changes: 36 additions & 8 deletions packages/typespec-ts/src/metadata/build-ts-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,38 @@ export function buildTsConfig(model: ClientModel) {
};
}

/**
* Computes the `include` list for the `config/tsconfig.src.*.json` files.
*
* The generated `warp.config.yml` exposes every client's entry point as a
* separate public subpath export (e.g. `./devCenter` -> `./src/devCenter/index.ts`).
* warp validates those declared exports against the emitted `dist` output, so
* each exported source file must be a TypeScript project input; otherwise the
* per-client barrels are never emitted and the build fails with `DIST_MISSING`.
*
* We therefore derive the `include` list from the same `exports` map used to
* build `warp.config.yml`, keeping the two in sync. The root `../src/index.ts`
* is always included as a fallback.
*/
Comment thread
kazrael2119 marked this conversation as resolved.
export function getSrcIncludePaths(exports?: Record<string, string>): string[] {
const includes = new Set<string>(["../src/index.ts"]);
if (exports) {
for (const value of Object.values(exports)) {
if (typeof value === "string" && value.endsWith(".ts")) {
// Export values are relative to the package root (e.g. "./src/foo/index.ts"),
// whereas tsconfig.src.*.json live in the `config/` subfolder, so they are
// referenced as "../src/foo/index.ts".
Comment thread
kazrael2119 marked this conversation as resolved.
Outdated
includes.add(value.replace(/^\.\//, "../"));
}
}
}
return Array.from(includes);
}

/**
* Builds config/tsconfig.src.esm.json — extends eng/tsconfigs/src.esm.json
*/
export function buildTsSrcEsmConfig() {
export function buildTsSrcEsmConfig(exports?: Record<string, string>) {
return {
path: "config/tsconfig.src.esm.json",
content: JSON.stringify(
Expand All @@ -64,7 +92,7 @@ export function buildTsSrcEsmConfig() {
compilerOptions: {
resolveJsonModule: true,
},
include: ["../src/index.ts"],
include: getSrcIncludePaths(exports),
},
null,
2,
Expand All @@ -75,7 +103,7 @@ export function buildTsSrcEsmConfig() {
/**
* Builds config/tsconfig.src.browser.json — extends eng/tsconfigs/src.browser.json
*/
export function buildTsSrcBrowserConfig() {
export function buildTsSrcBrowserConfig(exports?: Record<string, string>) {
return {
path: "config/tsconfig.src.browser.json",
content: JSON.stringify(
Expand All @@ -84,7 +112,7 @@ export function buildTsSrcBrowserConfig() {
compilerOptions: {
resolveJsonModule: true,
},
include: ["../src/index.ts"],
include: getSrcIncludePaths(exports),
},
null,
2,
Expand All @@ -95,7 +123,7 @@ export function buildTsSrcBrowserConfig() {
/**
* Builds config/tsconfig.src.react-native.json — extends eng/tsconfigs/src.react-native.json
*/
export function buildTsSrcReactNativeConfig() {
export function buildTsSrcReactNativeConfig(exports?: Record<string, string>) {
return {
path: "config/tsconfig.src.react-native.json",
content: JSON.stringify(
Expand All @@ -104,7 +132,7 @@ export function buildTsSrcReactNativeConfig() {
compilerOptions: {
resolveJsonModule: true,
},
include: ["../src/index.ts"],
include: getSrcIncludePaths(exports),
},
null,
2,
Expand All @@ -115,7 +143,7 @@ export function buildTsSrcReactNativeConfig() {
/**
* Builds config/tsconfig.src.cjs.json — extends eng/tsconfigs/src.cjs.json
*/
export function buildTsSrcCjsConfig() {
export function buildTsSrcCjsConfig(exports?: Record<string, string>) {
return {
path: "config/tsconfig.src.cjs.json",
content: JSON.stringify(
Expand All @@ -124,7 +152,7 @@ export function buildTsSrcCjsConfig() {
compilerOptions: {
resolveJsonModule: true,
},
include: ["../src/index.ts"],
include: getSrcIncludePaths(exports),
},
null,
2,
Expand Down
78 changes: 78 additions & 0 deletions packages/typespec-ts/test-next/unit/metadata/ts-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { describe, expect, it } from "vitest";

import {
buildTsSrcBrowserConfig,
buildTsSrcCjsConfig,
buildTsSrcEsmConfig,
buildTsSrcReactNativeConfig,
getSrcIncludePaths,
} from "../../../src/metadata/build-ts-config.js";

describe("tsconfig.src.*.json include generation", () => {
it("defaults to the root entry point when no exports are provided", () => {
expect(getSrcIncludePaths()).toEqual(["../src/index.ts"]);
expect(getSrcIncludePaths({})).toEqual(["../src/index.ts"]);
});

it("keeps the include list in sync with the warp exports for multi-client packages", () => {
// Mirrors the exports map produced for a multi-client package.
const exports = {
".": "./src/index.ts",
"./devCenter": "./src/devCenter/index.ts",
"./devCenter/api": "./src/devCenter/api/index.ts",
"./devBoxes": "./src/devBoxes/index.ts",
"./devBoxes/api": "./src/devBoxes/api/index.ts",
"./deploymentEnvironments": "./src/deploymentEnvironments/index.ts",
"./deploymentEnvironments/api": "./src/deploymentEnvironments/api/index.ts",
"./models": "./src/models/index.ts",
};

const include = getSrcIncludePaths(exports);

// Every per-client barrel must be a compilation input so it is emitted to dist.
expect(include).toContain("../src/index.ts");
expect(include).toContain("../src/devCenter/index.ts");
expect(include).toContain("../src/devBoxes/index.ts");
expect(include).toContain("../src/deploymentEnvironments/index.ts");
// The deeper api/model barrels are exported too, so they are included as well.
expect(include).toContain("../src/devCenter/api/index.ts");
expect(include).toContain("../src/models/index.ts");
});

it("ignores non-TypeScript export values (e.g. package.json)", () => {
const include = getSrcIncludePaths({
"./package.json": "./package.json",
".": "./src/index.ts",
});
expect(include).toEqual(["../src/index.ts"]);
});

it("does not emit duplicate include entries", () => {
const include = getSrcIncludePaths({
".": "./src/index.ts",
"./alias": "./src/index.ts",
});
expect(include).toEqual(["../src/index.ts"]);
});

it("propagates the exports into each per-target tsconfig", () => {
const exports = {
".": "./src/index.ts",
"./devCenter": "./src/devCenter/index.ts",
};

for (const build of [
buildTsSrcEsmConfig,
buildTsSrcBrowserConfig,
buildTsSrcReactNativeConfig,
buildTsSrcCjsConfig,
]) {
const result = build(exports);
const parsed = JSON.parse(result.content);
expect(parsed.include).toEqual(["../src/index.ts", "../src/devCenter/index.ts"]);
}
});
});
Loading