From 2b6a2c05f893217015fa6bdd61ba519aef0ea670 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 20:41:35 +0000 Subject: [PATCH 1/4] Initial plan From faed8079ba2212412469d308ce5728a123ae00e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 20:56:31 +0000 Subject: [PATCH 2/4] feat: add project boundary support to tspconfig.yaml Add `project` field to tspconfig.yaml that marks a config as a project boundary. When present, the directory containing the file becomes the project root with an explicit entrypoint declaration. Changes: - Add TypeSpecProjectConfig and TypeSpecRawProjectConfig types - Add project field to TypeSpecRawConfig and TypeSpecConfig - Add project property to JSON schema validation - Update config loader to parse and normalize project settings - Update CLI entrypoint resolution to check project tspconfig first - Update IDE entrypoint resolver to check project tspconfig first - Update tsp init scaffold to include project: true for non-library templates - Add comprehensive tests for all new functionality Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/52b4adb2-a8c8-4e7c-b9a6-8a3fcfdb2fb6 Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/src/config/config-loader.ts | 27 +++- packages/compiler/src/config/config-schema.ts | 16 +++ packages/compiler/src/config/types.ts | 38 ++++++ .../src/core/entrypoint-resolution.ts | 7 + packages/compiler/src/init/scaffold.ts | 7 + .../src/server/entrypoint-resolver.ts | 20 +++ packages/compiler/test/config/config.test.ts | 73 +++++++++++ .../scenarios/project-basic/tspconfig.yaml | 3 + .../project-entrypoint/tspconfig.yaml | 4 + .../services/orders/tspconfig.yaml | 4 + .../scenarios/project-nested/tspconfig.yaml | 3 + .../compiler/test/init/init-template.test.ts | 1 + .../test/server/completion.tspconfig.test.ts | 2 + .../test/server/entrypoint-resolver.test.ts | 121 +++++++++++++++++- 14 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/test/config/scenarios/project-basic/tspconfig.yaml create mode 100644 packages/compiler/test/config/scenarios/project-entrypoint/tspconfig.yaml create mode 100644 packages/compiler/test/config/scenarios/project-nested/services/orders/tspconfig.yaml create mode 100644 packages/compiler/test/config/scenarios/project-nested/tspconfig.yaml diff --git a/packages/compiler/src/config/config-loader.ts b/packages/compiler/src/config/config-loader.ts index 69ef9ed9643..3dcaf3bb318 100644 --- a/packages/compiler/src/config/config-loader.ts +++ b/packages/compiler/src/config/config-loader.ts @@ -9,7 +9,12 @@ import { getLocationInYamlScript } from "../yaml/index.js"; import { parseYaml } from "../yaml/parser.js"; import { YamlScript } from "../yaml/types.js"; import { TypeSpecConfigJsonSchema } from "./config-schema.js"; -import { TypeSpecConfig, TypeSpecRawConfig } from "./types.js"; +import { + TypeSpecConfig, + TypeSpecProjectConfig, + TypeSpecRawConfig, + TypeSpecRawProjectConfig, +} from "./types.js"; export const TypeSpecConfigFilename = "tspconfig.yaml"; @@ -180,13 +185,15 @@ async function loadConfigFile( const emit = data.emit; const options = data.options; + const configDir = getDirectoryPath(filename); return omitUndefined({ - projectRoot: getDirectoryPath(filename), + projectRoot: configDir, file: yamlScript, filename, diagnostics, extends: data.extends, + project: resolveProjectConfig(data.project, configDir), environmentVariables: data["environment-variables"], parameters: data.parameters, outputDir: data["output-dir"] ?? "{cwd}/tsp-output", @@ -199,6 +206,22 @@ async function loadConfigFile( }); } +/** + * Resolve the raw project config into a normalized project config with absolute paths. + */ +function resolveProjectConfig( + raw: TypeSpecRawProjectConfig | undefined, + configDir: string, +): TypeSpecProjectConfig | undefined { + if (raw === undefined) { + return undefined; + } + const entrypoint = raw === true ? "main.tsp" : (raw.entrypoint ?? "main.tsp"); + return { + entrypoint: resolvePath(configDir, entrypoint), + }; +} + export function validateConfigPathsAbsolute(config: TypeSpecConfig): readonly Diagnostic[] { const diagnostics: Diagnostic[] = []; diff --git a/packages/compiler/src/config/config-schema.ts b/packages/compiler/src/config/config-schema.ts index 0df1c8b9333..30938131e1a 100644 --- a/packages/compiler/src/config/config-schema.ts +++ b/packages/compiler/src/config/config-schema.ts @@ -18,6 +18,22 @@ export const TypeSpecConfigJsonSchema: JSONSchemaType = { type: "string", nullable: true, }, + project: { + oneOf: [ + { type: "boolean", const: true }, + { + type: "object", + properties: { + entrypoint: { + type: "string", + nullable: true, + }, + }, + additionalProperties: false, + required: [], + }, + ], + } as any, // AJV typing doesn't handle oneOf with mixed types well "environment-variables": { type: "object", nullable: true, diff --git a/packages/compiler/src/config/types.ts b/packages/compiler/src/config/types.ts index f38d07a1173..a7ab2ea1687 100644 --- a/packages/compiler/src/config/types.ts +++ b/packages/compiler/src/config/types.ts @@ -1,6 +1,30 @@ import type { Diagnostic, RuleRef } from "../core/types.js"; import type { YamlScript } from "../yaml/types.js"; +/** + * Resolved project configuration for a project boundary. + */ +export interface TypeSpecProjectConfig { + /** + * Resolved absolute path to the entrypoint file. + */ + entrypoint: string; +} + +/** + * Raw project configuration as provided in tspconfig.yaml. + * Can be `true` (shorthand for all defaults) or an object with explicit settings. + */ +export type TypeSpecRawProjectConfig = + | true + | { + /** + * Main TypeSpec file for this project, relative to config directory. + * @default "main.tsp" + */ + entrypoint?: string; + }; + /** * Represent the normalized user configuration. */ @@ -28,6 +52,13 @@ export interface TypeSpecConfig { */ extends?: string; + /** + * Resolved project configuration. + * When present, this config defines a project boundary and the directory + * containing this file is the project root. + */ + project?: TypeSpecProjectConfig; + /** * Environment variables configuration */ @@ -76,6 +107,13 @@ export interface TypeSpecConfig { */ export interface TypeSpecRawConfig { extends?: string; + + /** + * Marks this configuration as a project boundary. + * When present, the directory containing this file is the project root. + */ + project?: TypeSpecRawProjectConfig; + "environment-variables"?: Record; parameters?: Record; diff --git a/packages/compiler/src/core/entrypoint-resolution.ts b/packages/compiler/src/core/entrypoint-resolution.ts index b9d34c17869..c1d27ebf602 100644 --- a/packages/compiler/src/core/entrypoint-resolution.ts +++ b/packages/compiler/src/core/entrypoint-resolution.ts @@ -1,5 +1,6 @@ import { doIO, loadFile } from "../utils/io.js"; import { resolveTspMain } from "../utils/misc.js"; +import { loadTypeSpecConfigForPath } from "../config/config-loader.js"; import { DiagnosticHandler } from "./diagnostics.js"; import { resolvePath } from "./path-utils.js"; import { CompilerHost } from "./types.js"; @@ -32,6 +33,12 @@ export async function resolveTypeSpecEntrypointForDir( dir: string, reportDiagnostic: DiagnosticHandler, ): Promise { + // Check for project tspconfig.yaml first — highest priority + const config = await loadTypeSpecConfigForPath(host, dir, false, false); + if (config.project) { + return config.project.entrypoint; + } + const pkgJsonPath = resolvePath(dir, "package.json"); const [pkg] = await loadFile(host, pkgJsonPath, JSON.parse, reportDiagnostic, { allowFileNotFound: true, diff --git a/packages/compiler/src/init/scaffold.ts b/packages/compiler/src/init/scaffold.ts index c3652012da1..2b93ffd6cc3 100644 --- a/packages/compiler/src/init/scaffold.ts +++ b/packages/compiler/src/init/scaffold.ts @@ -180,6 +180,13 @@ async function writeConfig(host: SystemHost, config: ScaffoldingConfig) { Object.entries(config.emitters).map(([key, emitter]) => [key, emitter.options]), ); } + + // Mark generated projects as project boundaries + if (config.template.target !== "library") { + rawConfig ??= {}; + rawConfig.project = true; + } + const content = rawConfig ? stringify(rawConfig) : placeholderConfig; return host.writeFile(joinPaths(config.directory, TypeSpecConfigFilename), content); } diff --git a/packages/compiler/src/server/entrypoint-resolver.ts b/packages/compiler/src/server/entrypoint-resolver.ts index e60e82d74ce..cad45f2db68 100644 --- a/packages/compiler/src/server/entrypoint-resolver.ts +++ b/packages/compiler/src/server/entrypoint-resolver.ts @@ -3,6 +3,7 @@ import { getDirectoryPath, joinPaths } from "../core/path-utils.js"; import { SystemHost, Diagnostic as TypeSpecDiagnostic } from "../core/types.js"; import { doIO, loadFile } from "../utils/io.js"; import { resolveTspMain } from "../utils/misc.js"; +import { TypeSpecConfigFilename, loadTypeSpecConfigFile } from "../config/config-loader.js"; import { debugLoggers } from "./debug.js"; import { FileSystemCache } from "./file-system-cache.js"; import { ServerLog } from "./types.js"; @@ -27,6 +28,25 @@ export async function resolveEntrypointFile( } while (true) { + // Check for project tspconfig.yaml first — highest priority + const tspConfigPath = joinPaths(dir, TypeSpecConfigFilename); + const tspConfigStat = await doIO( + () => host.stat(tspConfigPath), + tspConfigPath, + logMainFileSearchDiagnostic, + options, + ); + if (tspConfigStat?.isFile()) { + const config = await loadTypeSpecConfigFile(host, tspConfigPath); + if (config.diagnostics.length === 0 && config.project) { + logDebug({ + level: "debug", + message: `project entrypoint resolved from tspconfig.yaml (${tspConfigPath}) as ${config.project.entrypoint}`, + }); + return config.project.entrypoint; + } + } + let pkg: any; const pkgPath = joinPaths(dir, "package.json"); const cached = await fileSystemCache?.get(pkgPath); diff --git a/packages/compiler/test/config/config.test.ts b/packages/compiler/test/config/config.test.ts index a7c058b416b..8eaa951463b 100644 --- a/packages/compiler/test/config/config.test.ts +++ b/packages/compiler/test/config/config.test.ts @@ -134,6 +134,57 @@ describe("compiler: config file loading", () => { }); }); + describe("project config", () => { + const loadFullConfig = async (path: string, lookup: boolean = true) => { + const fullPath = join(scenarioRoot, path); + return loadTypeSpecConfigForPath(NodeHost, fullPath, false, lookup); + }; + + it("loads project: true shorthand", async () => { + const config = await loadFullConfig("project-basic", false); + strictEqual(config.diagnostics.length, 0); + strictEqual(config.project !== undefined, true); + strictEqual(config.project!.entrypoint.endsWith("main.tsp"), true); + deepStrictEqual(config.emit, ["openapi"]); + }); + + it("loads project with explicit entrypoint", async () => { + const config = await loadFullConfig("project-entrypoint", false); + strictEqual(config.diagnostics.length, 0); + strictEqual(config.project !== undefined, true); + strictEqual(config.project!.entrypoint.endsWith("src/service.tsp"), true); + deepStrictEqual(config.emit, ["openapi"]); + }); + + it("resolves entrypoint as absolute path relative to config directory", async () => { + const config = await loadFullConfig("project-entrypoint", false); + const expectedSuffix = join("project-entrypoint", "src", "service.tsp"); + strictEqual(config.project!.entrypoint.endsWith(expectedSuffix), true); + }); + + it("config without project field has no project property", async () => { + const config = await loadFullConfig("simple", false); + strictEqual(config.project, undefined); + }); + + it("loads nested project configs independently", async () => { + const rootConfig = await loadFullConfig("project-nested", false); + const nestedConfig = await loadFullConfig("project-nested/services/orders", false); + + strictEqual(rootConfig.project !== undefined, true); + strictEqual(nestedConfig.project !== undefined, true); + + strictEqual(rootConfig.project!.entrypoint.endsWith("project-nested/main.tsp"), true); + strictEqual( + nestedConfig.project!.entrypoint.endsWith("services/orders/main.tsp"), + true, + ); + + deepStrictEqual(rootConfig.emit, ["openapi"]); + deepStrictEqual(nestedConfig.emit, ["@typespec/http-client-csharp"]); + }); + }); + describe("validation", () => { const validator = createJSONSchemaValidator(TypeSpecConfigJsonSchema); const file = createSourceFile("", ""); @@ -168,5 +219,27 @@ describe("compiler: config file loading", () => { it("succeeds if config is valid", () => { deepStrictEqual(validate({ options: { openapi: {} } }), []); }); + + it("succeeds with project: true", () => { + deepStrictEqual(validate({ project: true }), []); + }); + + it("succeeds with project object with entrypoint", () => { + deepStrictEqual(validate({ project: { entrypoint: "src/main.tsp" } }), []); + }); + + it("succeeds with project object without entrypoint", () => { + deepStrictEqual(validate({ project: {} }), []); + }); + + it("fails with project: false", () => { + const diagnostics = validate({ project: false } as any); + strictEqual(diagnostics.length > 0, true); + }); + + it("fails with project containing unknown properties", () => { + const diagnostics = validate({ project: { unknown: "value" } } as any); + strictEqual(diagnostics.length > 0, true); + }); }); }); diff --git a/packages/compiler/test/config/scenarios/project-basic/tspconfig.yaml b/packages/compiler/test/config/scenarios/project-basic/tspconfig.yaml new file mode 100644 index 00000000000..89f3d3de14c --- /dev/null +++ b/packages/compiler/test/config/scenarios/project-basic/tspconfig.yaml @@ -0,0 +1,3 @@ +project: true +emit: + - openapi diff --git a/packages/compiler/test/config/scenarios/project-entrypoint/tspconfig.yaml b/packages/compiler/test/config/scenarios/project-entrypoint/tspconfig.yaml new file mode 100644 index 00000000000..66744e4d396 --- /dev/null +++ b/packages/compiler/test/config/scenarios/project-entrypoint/tspconfig.yaml @@ -0,0 +1,4 @@ +project: + entrypoint: src/service.tsp +emit: + - openapi diff --git a/packages/compiler/test/config/scenarios/project-nested/services/orders/tspconfig.yaml b/packages/compiler/test/config/scenarios/project-nested/services/orders/tspconfig.yaml new file mode 100644 index 00000000000..0a5458c72b0 --- /dev/null +++ b/packages/compiler/test/config/scenarios/project-nested/services/orders/tspconfig.yaml @@ -0,0 +1,4 @@ +project: + entrypoint: main.tsp +emit: + - "@typespec/http-client-csharp" diff --git a/packages/compiler/test/config/scenarios/project-nested/tspconfig.yaml b/packages/compiler/test/config/scenarios/project-nested/tspconfig.yaml new file mode 100644 index 00000000000..89f3d3de14c --- /dev/null +++ b/packages/compiler/test/config/scenarios/project-nested/tspconfig.yaml @@ -0,0 +1,3 @@ +project: true +emit: + - openapi diff --git a/packages/compiler/test/init/init-template.test.ts b/packages/compiler/test/init/init-template.test.ts index dc9944b37cf..b45346353f5 100644 --- a/packages/compiler/test/init/init-template.test.ts +++ b/packages/compiler/test/init/init-template.test.ts @@ -123,6 +123,7 @@ it("specifying both config and emitters merge the 2", async () => { const output = getOutputFile("tspconfig.yaml"); ok(output); expect(parse(output)).toEqual({ + project: true, "warn-as-error": true, emit: ["foo"], options: { diff --git a/packages/compiler/test/server/completion.tspconfig.test.ts b/packages/compiler/test/server/completion.tspconfig.test.ts index 9183628a2bd..8d8fc2fc97c 100644 --- a/packages/compiler/test/server/completion.tspconfig.test.ts +++ b/packages/compiler/test/server/completion.tspconfig.test.ts @@ -17,6 +17,7 @@ const rootOptions = [ "emit", "options", "linter", + "project", ]; describe("Test completion items for root options", () => { @@ -443,6 +444,7 @@ describe("Test completion items for extends", () => { "options", "output-dir", "parameters", + "project", "trace", "warn-as-error", ], diff --git a/packages/compiler/test/server/entrypoint-resolver.test.ts b/packages/compiler/test/server/entrypoint-resolver.test.ts index 081bc41f711..7e14e015d21 100644 --- a/packages/compiler/test/server/entrypoint-resolver.test.ts +++ b/packages/compiler/test/server/entrypoint-resolver.test.ts @@ -1,4 +1,4 @@ -import { afterEach, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { joinPaths } from "../../src/core/path-utils.js"; import type { SystemHost } from "../../src/core/types.js"; import { resolveEntrypointFile } from "../../src/server/entrypoint-resolver.js"; @@ -156,3 +156,122 @@ it("uses main.tsp as default entrypoint when entrypoints is null or undefined an expect(resultForUndefined).toBe(expectedMainTsp); }); + +describe("project tspconfig entrypoint resolution", () => { + it("resolves entrypoint from project tspconfig.yaml with default main.tsp", async () => { + const host = createMockHost(); + const dir = "/project"; + const filePath = joinPaths(dir, "src", "doc.tsp"); + const tspConfigPath = joinPaths(dir, "tspconfig.yaml"); + const expectedEntrypoint = joinPaths(dir, "main.tsp"); + + vi.mocked(host.stat).mockImplementation(async (path) => { + if (path === tspConfigPath) return { isFile: () => true } as any; + return { isFile: () => false } as any; + }); + + vi.mocked(host.readFile).mockImplementation(async (path: string) => { + if (path === tspConfigPath) { + return { text: "project: true\n", path: tspConfigPath } as any; + } + if (path.endsWith("package.json")) { + return { text: "{}", path } as any; + } + throw new Error("File not found"); + }); + + const { log } = createLogger(); + const result = await resolveEntrypointFile(host, undefined, filePath, undefined, log); + expect(result).toBe(expectedEntrypoint); + }); + + it("resolves entrypoint from project tspconfig.yaml with custom entrypoint", async () => { + const host = createMockHost(); + const dir = "/project"; + const filePath = joinPaths(dir, "src", "doc.tsp"); + const tspConfigPath = joinPaths(dir, "tspconfig.yaml"); + const expectedEntrypoint = joinPaths(dir, "src/service.tsp"); + + vi.mocked(host.stat).mockImplementation(async (path) => { + if (path === tspConfigPath) return { isFile: () => true } as any; + return { isFile: () => false } as any; + }); + + vi.mocked(host.readFile).mockImplementation(async (path: string) => { + if (path === tspConfigPath) { + return { + text: "project:\n entrypoint: src/service.tsp\n", + path: tspConfigPath, + } as any; + } + if (path.endsWith("package.json")) { + return { text: "{}", path } as any; + } + throw new Error("File not found"); + }); + + const { log } = createLogger(); + const result = await resolveEntrypointFile(host, undefined, filePath, undefined, log); + expect(result).toBe(expectedEntrypoint); + }); + + it("project tspconfig takes priority over tspMain in package.json", async () => { + const host = createMockHost(); + const dir = "/project"; + const filePath = joinPaths(dir, "src", "doc.tsp"); + const tspConfigPath = joinPaths(dir, "tspconfig.yaml"); + const expectedEntrypoint = joinPaths(dir, "main.tsp"); + + vi.mocked(host.stat).mockImplementation(async (path) => { + if (path === tspConfigPath) return { isFile: () => true } as any; + if (path === joinPaths(dir, "other.tsp")) return { isFile: () => true } as any; + return { isFile: () => false } as any; + }); + + vi.mocked(host.readFile).mockImplementation(async (path: string) => { + if (path === tspConfigPath) { + return { text: "project: true\n", path: tspConfigPath } as any; + } + if (path.endsWith("package.json")) { + // package.json with tspMain pointing to a different file + return { text: JSON.stringify({ tspMain: "other.tsp" }), path } as any; + } + throw new Error("File not found"); + }); + + const { log } = createLogger(); + const result = await resolveEntrypointFile(host, undefined, filePath, undefined, log); + // Should use project config's entrypoint (main.tsp) not tspMain (other.tsp) + expect(result).toBe(expectedEntrypoint); + }); + + it("falls back to normal resolution when tspconfig has no project field", async () => { + const host = createMockHost(); + const dir = "/project"; + const filePath = joinPaths(dir, "src", "doc.tsp"); + const tspConfigPath = joinPaths(dir, "tspconfig.yaml"); + const expectedEntrypoint = joinPaths(dir, "main.tsp"); + + vi.mocked(host.stat).mockImplementation(async (path) => { + if (path === tspConfigPath) return { isFile: () => true } as any; + if (path === expectedEntrypoint) return { isFile: () => true } as any; + return { isFile: () => false } as any; + }); + + vi.mocked(host.readFile).mockImplementation(async (path: string) => { + if (path === tspConfigPath) { + // No project field — just build config + return { text: 'emit:\n - "openapi"\n', path: tspConfigPath } as any; + } + if (path.endsWith("package.json")) { + return { text: "{}", path } as any; + } + throw new Error("File not found"); + }); + + const { log } = createLogger(); + const result = await resolveEntrypointFile(host, undefined, filePath, undefined, log); + // Falls back to finding main.tsp via the normal entrypoint search + expect(result).toBe(expectedEntrypoint); + }); +}); From aaca4b18cce3fd4b4d526b6346def6e996126cfb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 21:00:05 +0000 Subject: [PATCH 3/4] chore: format code and add changelog entry Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/52b4adb2-a8c8-4e7c-b9a6-8a3fcfdb2fb6 Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...design-tspconfig-project-structure-2026-3-1-20-59-54.md | 7 +++++++ packages/compiler/src/core/entrypoint-resolution.ts | 2 +- packages/compiler/src/server/entrypoint-resolver.ts | 2 +- packages/compiler/test/config/config.test.ts | 5 +---- 4 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 .chronus/changes/copilot-design-tspconfig-project-structure-2026-3-1-20-59-54.md diff --git a/.chronus/changes/copilot-design-tspconfig-project-structure-2026-3-1-20-59-54.md b/.chronus/changes/copilot-design-tspconfig-project-structure-2026-3-1-20-59-54.md new file mode 100644 index 00000000000..e7e4bd1b98a --- /dev/null +++ b/.chronus/changes/copilot-design-tspconfig-project-structure-2026-3-1-20-59-54.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Add `project` field to tspconfig.yaml for defining project boundaries and explicit entrypoint declaration \ No newline at end of file diff --git a/packages/compiler/src/core/entrypoint-resolution.ts b/packages/compiler/src/core/entrypoint-resolution.ts index c1d27ebf602..681f0f06a01 100644 --- a/packages/compiler/src/core/entrypoint-resolution.ts +++ b/packages/compiler/src/core/entrypoint-resolution.ts @@ -1,6 +1,6 @@ +import { loadTypeSpecConfigForPath } from "../config/config-loader.js"; import { doIO, loadFile } from "../utils/io.js"; import { resolveTspMain } from "../utils/misc.js"; -import { loadTypeSpecConfigForPath } from "../config/config-loader.js"; import { DiagnosticHandler } from "./diagnostics.js"; import { resolvePath } from "./path-utils.js"; import { CompilerHost } from "./types.js"; diff --git a/packages/compiler/src/server/entrypoint-resolver.ts b/packages/compiler/src/server/entrypoint-resolver.ts index cad45f2db68..5324d2ad5bc 100644 --- a/packages/compiler/src/server/entrypoint-resolver.ts +++ b/packages/compiler/src/server/entrypoint-resolver.ts @@ -1,9 +1,9 @@ +import { TypeSpecConfigFilename, loadTypeSpecConfigFile } from "../config/config-loader.js"; import { formatDiagnostic } from "../core/logger/console-sink.js"; import { getDirectoryPath, joinPaths } from "../core/path-utils.js"; import { SystemHost, Diagnostic as TypeSpecDiagnostic } from "../core/types.js"; import { doIO, loadFile } from "../utils/io.js"; import { resolveTspMain } from "../utils/misc.js"; -import { TypeSpecConfigFilename, loadTypeSpecConfigFile } from "../config/config-loader.js"; import { debugLoggers } from "./debug.js"; import { FileSystemCache } from "./file-system-cache.js"; import { ServerLog } from "./types.js"; diff --git a/packages/compiler/test/config/config.test.ts b/packages/compiler/test/config/config.test.ts index 8eaa951463b..861c0029953 100644 --- a/packages/compiler/test/config/config.test.ts +++ b/packages/compiler/test/config/config.test.ts @@ -175,10 +175,7 @@ describe("compiler: config file loading", () => { strictEqual(nestedConfig.project !== undefined, true); strictEqual(rootConfig.project!.entrypoint.endsWith("project-nested/main.tsp"), true); - strictEqual( - nestedConfig.project!.entrypoint.endsWith("services/orders/main.tsp"), - true, - ); + strictEqual(nestedConfig.project!.entrypoint.endsWith("services/orders/main.tsp"), true); deepStrictEqual(rootConfig.emit, ["openapi"]); deepStrictEqual(nestedConfig.emit, ["@typespec/http-client-csharp"]); From 7dac40b1d85c9408bcc90ae9ad413100d8950542 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 21:36:31 +0000 Subject: [PATCH 4/4] fix: remove project: true from tsp init scaffold to fix e2e tests The scaffolded tspconfig.yaml with project: true was rejected by the published compiler version used in e2e tests (which doesn't know about the new field). Remove the automatic addition from scaffold since users can manually opt-in to project: true once the feature is released. Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/e47a1ecd-c8a3-4d1f-af77-277a4b10ea04 Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/src/init/scaffold.ts | 7 ------- packages/compiler/test/init/init-template.test.ts | 1 - 2 files changed, 8 deletions(-) diff --git a/packages/compiler/src/init/scaffold.ts b/packages/compiler/src/init/scaffold.ts index 2b93ffd6cc3..c3652012da1 100644 --- a/packages/compiler/src/init/scaffold.ts +++ b/packages/compiler/src/init/scaffold.ts @@ -180,13 +180,6 @@ async function writeConfig(host: SystemHost, config: ScaffoldingConfig) { Object.entries(config.emitters).map(([key, emitter]) => [key, emitter.options]), ); } - - // Mark generated projects as project boundaries - if (config.template.target !== "library") { - rawConfig ??= {}; - rawConfig.project = true; - } - const content = rawConfig ? stringify(rawConfig) : placeholderConfig; return host.writeFile(joinPaths(config.directory, TypeSpecConfigFilename), content); } diff --git a/packages/compiler/test/init/init-template.test.ts b/packages/compiler/test/init/init-template.test.ts index b45346353f5..dc9944b37cf 100644 --- a/packages/compiler/test/init/init-template.test.ts +++ b/packages/compiler/test/init/init-template.test.ts @@ -123,7 +123,6 @@ it("specifying both config and emitters merge the 2", async () => { const output = getOutputFile("tspconfig.yaml"); ok(output); expect(parse(output)).toEqual({ - project: true, "warn-as-error": true, emit: ["foo"], options: {