Skip to content
Open
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:
- "@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).
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.
*/
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".
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