-
Notifications
You must be signed in to change notification settings - Fork 351
tspconfig.yaml project #10240
Description
Design: Project tspconfig.yaml
Status
Draft — Seeking feedback on project boundary semantics and open questions.
Related: PR #9886 — Feature Flags via Directives (Alternative D)
Problem
Today, tspconfig.yaml serves as a build/output configuration — it configures linter rules, emit targets, and output directories. It does not define a project boundary. This creates several problems:
- No project identity: A single TypeSpec project may use multiple tspconfig files (one per emit target), or share a config across projects. There is no authoritative "this is a TypeSpec project" marker.
- Entrypoint ambiguity: The IDE walks up directories looking for
package.jsonwithexports["typespec"]/tspMain(legacy) ormain.tsp. There's no way to explicitly declare the entrypoint in config. - No project-scoped settings: Settings like feature flags (PR #9886) need a project-level scope. Today's tspconfig can't serve this role because it's build config, not project config.
- Monorepo pain: In repositories with multiple TypeSpec services, there's no clean way to define boundaries between projects. This also causes issues in the Language server to figure out which files belong to which project.
Goals
- Define a project boundary mechanism using
tspconfig.yaml - Allow explicit entrypoint declaration
- Establish a foundation for project-scoped settings (feature flags, etc.)
- Maintain full backward compatibility
Non-goals (for this proposal)
- Workspace-level configuration (listing all projects in a monorepo)
- Project references (cross-project dependency declarations)
- Feature flag implementation details (covered in PR #9886)
Design
1. Project marker
A tspconfig.yaml becomes a project boundary when it contains a project marker. The directory containing this file becomes the project root. When no project marker is present, the tspconfig is a regular build configuration (backward compatible).
Decision needed: Which syntax should mark a config as a project?
Option A: project field
A top-level project field serves as both the marker and the namespace for project settings:
# Full form
project:
entrypoint: main.tsp
# Shorthand (all defaults)
project: true- Pro: Project settings (entrypoint, future feature flags) are grouped under one key — clean namespace separation from build config.
- Pro: Extensible — adding
features,references, etc. as nested properties is natural. - Con:
project: true(marker) vsproject: { ... }(config object) is a dual type that can feel inconsistent.
Option B: kind field
A top-level kind discriminator marks the config type, with project settings at the top level:
kind: project
entrypoint: main.tsp- Pro: Naturally extensible to other config kinds in the future (e.g.,
kind: workspace). - Pro: Flat top-level structure feels simpler for the common case.
- Con: Project-scoped settings (entrypoint, future feature flags) live at the top level alongside build config fields — no clear separation.
2. Entrypoint resolution
The project.entrypoint field declares the main file for the project, relative to the project root:
# Default — looks for main.tsp in project root
project:
entrypoint: main.tsp# Explicit — custom path
project:
entrypoint: src/service.tspIDE behavior change
Current behavior (packages/compiler/src/server/entrypoint-resolver.ts):
- Walk up from current file's directory
- Check
package.jsonforexports["typespec"]ortspMain(legacy) - Look for
main.tsp - Repeat in parent directories until filesystem root
- Fall back to the file itself
New behavior:
- Walk up from current file's directory
- Check for project tspconfig.yaml — if found, use
project.entrypoint(default:main.tsp) - Check
package.jsonforexports["typespec"]ortspMain(legacy) - Look for
main.tsp - Repeat in parent directories until filesystem root
- Fall back to the file itself
The project tspconfig check is inserted as the highest priority resolution step at each directory level. If a project tspconfig is found, the walk stops — the entrypoint is definitively resolved.
CLI behavior change
Current behavior (packages/compiler/src/core/entrypoint-resolution.ts):
When tsp compile <dir> receives a directory:
- Check
package.jsonforexports["typespec"]ortspMain(legacy) - Fall back to
main.tsp
New behavior:
- Check for project tspconfig.yaml — if found, use
project.entrypoint - Check
package.jsonforexports["typespec"]ortspMain(legacy) - Fall back to
main.tsp
Note: The CLI does not walk up directories (unlike the IDE). This is unchanged.
3. Project boundary semantics
A project boundary defines the scope of project-level settings. Key rules:
- A project includes all TypeSpec files under its root directory, excluding files that fall under a nested project root (those belong to the nested project).
- When the compiler loads a library (e.g., from
node_modules), it crosses into that library's project boundary. The library's own project tspconfig (if any) takes effect for its files. - Project-level settings (like feature flags) do not propagate across project boundaries.
Example: Monorepo layout
repo/
├── tspconfig.yaml # project: { entrypoint: main.tsp } ← Project A
├── main.tsp
├── models/
│ └── widget.tsp # part of Project A
├── services/
│ └── orders/
│ ├── tspconfig.yaml # project: { entrypoint: main.tsp } ← Project B
│ └── main.tsp
└── node_modules/
└── @typespec/http/
├── package.json # exports: { "typespec": "./lib/main.tsp" }
├── tspconfig.yaml # project: { features: { ... } } ← Library project
└── lib/
└── main.tsp
widget.tspbelongs to Project Aservices/orders/main.tspbelongs to Project B (nested project boundary)@typespec/httphas its own project boundary as a library
4. Relationship between project tspconfig and build tspconfig
The project tspconfig is also a build config. All existing fields (emit, options, linter, etc.) continue to work alongside the project field:
project:
entrypoint: main.tsp
emit:
- "@typespec/openapi3"
options:
"@typespec/openapi3":
emitter-output-dir: "{project-root}/out"
linter:
extends:
- "@typespec/http/all"Multiple build configurations for the same project
To support different emit targets for the same project, use additional non-project tspconfig files that extend the project config:
my-service/
├── tspconfig.yaml # project config + default build config
├── tspconfig.openapi.yaml # build-only override (no project field)
├── tspconfig.csharp.yaml # build-only override (no project field)
└── main.tsp
# tspconfig.openapi.yaml
extends: ./tspconfig.yaml # inherits project settings + base config
emit:
- "@typespec/openapi3"# tspconfig.csharp.yaml
extends: ./tspconfig.yaml
emit:
- "@typespec/http-client-csharp"Usage:
tsp compile . # uses tspconfig.yaml (project + default build)
tsp compile --config tspconfig.openapi.yaml # uses openapi override
tsp compile --config tspconfig.csharp.yaml # uses csharp overrideA non-project config that extends a project config inherits the project boundary — it knows where the project root is and what the entrypoint is.
5. Libraries and project tspconfig
Libraries (reusable TypeSpec packages distributed via npm) can define a project tspconfig. This is important for:
- Scoping future feature flags to the library's own code
- Providing an authoritative project boundary for the library
For libraries, package.json exports["typespec"] (or tspMain for legacy packages) remains the authoritative entrypoint mechanism:
node_modules/@typespec/http/
├── package.json # exports: { "typespec": "./lib/main.tsp" }
├── tspconfig.yaml # project: { features: { ... } }
└── lib/
└── main.tsp
Resolution priority for library entrypoint:
package.jsonexports["typespec"](primary, authoritative for libraries)package.jsontspMain(legacy fallback)- Project tspconfig
project.entrypoint(new, lowest priority for libraries)
Rationale: package.json exports["typespec"] is the current convention for npm packages and must remain authoritative to avoid breaking changes. tspMain is supported as a legacy fallback.
Entrypoint consistency validation
When a library defines both a package.json entrypoint and a project tspconfig with project.entrypoint, these must agree. Rather than maintaining two sources of truth, the exports["typespec"] value could point to the tspconfig.yaml itself, which in turn declares the entrypoint. This way the tspconfig becomes the single authority for the library's TypeSpec configuration.
// @typespec/http package.json
{
"exports": {
".": { "typespec": "./tspconfig.yaml" }
}
}# @typespec/http tspconfig.yaml
project:
entrypoint: lib/main.tspThis eliminates the consistency problem — there's only one place declaring the entrypoint — and naturally gives the library a project boundary with all its settings (feature flags, etc.).
If the exports["typespec"] value points to a .tsp file (current behavior) or tspMain is used, it continues to work as before — backward compatible.
Subpath exports and project boundaries
Some libraries expose multiple TypeSpec subpath exports. For example, @typespec/http exports both a root entrypoint and a ./streams subpath.
With the tspconfig-as-export approach, there are two options:
Option A: Single tspconfig, multiple subpath entrypoints listed in package.json
Each subpath export still points to its .tsp file directly. We load a sibling to package.json
{
"exports": {
".": { "typespec": "./main.tsp" },
"./streams": { "typespec": "./lib/streams/main.tsp" }
}
}- Pro: Simple; one project boundary per npm package; feature flags apply uniformly.
- Con: Subpath exports bypass the tspconfig — they don't get project-level settings unless the compiler infers they belong to the same project boundary.
Option B: Nested tspconfig per subpath
Each subpath export points to its own tspconfig:
{
"exports": {
".": { "typespec": "./tspconfig.yaml" },
"./streams": { "typespec": "./lib/streams/tspconfig.yaml" }
}
}- Pro: Each subpath has its own project settings; consistent model — every export goes through a tspconfig.
- Con: More files to maintain; unclear if subpaths should really be independent projects — they ship as one npm package.
Leaning toward: Option A for simplicity. Subpath exports are part of the same library and should inherit the root project boundary. The compiler can infer that any .tsp file under the package root (without its own nested tspconfig) belongs to the root project.
6. Feature flags (future extension preview)
With the project boundary established, feature flags (PR #9886, Alternative D) can be scoped to the project:
project:
entrypoint: main.tsp
features:
internal-modifier: on
new-decorators: offSchema Changes
TypeSpecRawConfig additions
interface TypeSpecRawConfig {
// ... existing fields ...
/** Marks this config as a project boundary */
project?:
| true
| {
/** Main TypeSpec file, relative to config directory. Default: "main.tsp" */
entrypoint?: string;
// Future:
// features?: Record<string, "on" | "off">;
};
}TypeSpecConfig additions
interface TypeSpecConfig {
// ... existing fields ...
/** Resolved project configuration, undefined if not a project config */
project?: {
/** Resolved absolute path to the entrypoint file */
entrypoint: string;
// Future:
// features: Map<string, boolean>;
};
}JSON Schema addition
{
"project": {
"oneOf": [
{ "type": "boolean", "const": true },
{
"type": "object",
"properties": {
"entrypoint": {
"type": "string",
"description": "Main TypeSpec file for this project, relative to config directory",
"default": "main.tsp"
}
},
"additionalProperties": false
}
],
"description": "Marks this configuration as a project boundary. When present, the directory containing this file is the project root."
}
}Open Questions
Q1: Config merging with nested non-project tspconfigs
If a non-project tspconfig exists inside a project boundary (not as a sibling, but in a subdirectory), how should it behave?
my-project/
├── tspconfig.yaml # project config
├── main.tsp
└── subdir/
├── tspconfig.yaml # non-project config — what happens?
└── helper.tsp
Options:
| Option | Behavior | Pros | Cons |
|---|---|---|---|
| A) Ignore | Non-project tspconfigs inside a project are only used when explicitly referenced via --config or extends |
Simple, predictable | May surprise users who expect config inheritance |
| B) Merge | Walk up and merge build settings, but project-level settings only come from the project config | Familiar pattern | Complex merging rules, potential for confusion |
| C) Warn | Emit a diagnostic warning about the ambiguous nested config | Helps users notice misconfiguration | Could be noisy |
Leaning toward: Option A — non-project tspconfigs inside a project are opt-in via --config or extends. Simplest mental model.
Q2: --config pointing to a project file
Should tsp compile --config path/to/tspconfig.yaml be allowed when the target config is a project file?
Options:
| Option | Behavior |
|---|---|
| A) Yes, always | --config works regardless of project field presence |
| B) Yes, with constraints | Only valid if the compile target is within that project's boundary |
| C) No | --config can only point to non-project build configs |
Leaning toward: Option B — prevents confusing cross-project compilation while maintaining flexibility.
Implementation Phases
Phase 1: Project boundary + entrypoint
- Add
projectfield to config schema, JSON schema, and TypeScript types - Update config loader (
config-loader.ts) to parse and normalize project settings - Update IDE entrypoint resolver (
entrypoint-resolver.ts) to check project tspconfig first - Update CLI entrypoint resolution (
entrypoint-resolution.ts) for directories - Add tests for project config loading, entrypoint resolution, nested projects
- Update
tsp inittemplates to includeproject: true
Phase 2: Build config interaction
- Define and implement behavior for
extendswith project configs - Handle
--configflag validation with project boundaries - Define merging semantics for nested non-project configs
- Update documentation (configuration handbook)
Phase 3: Feature flags
Depends on PR #9886 decisions.
- Add
featuresfield to project config schema - Implement feature flag scoping within project boundaries
- Implement cross-boundary isolation (features don't propagate into dependencies)
- Compiler API for registering known features and querying enablement at source locations
Migration / Backward Compatibility
- No breaking changes: Existing tspconfig.yaml files without
projectcontinue to work identically. - Gradual adoption: Projects can add
project: trueat their own pace. - IDE graceful degradation: If no project config is found, fall back to current behavior (walk up looking for
package.jsonexports["typespec"]/tspMainormain.tsp). tsp init: Updated templates should includeproject: truein generated tspconfig.yaml to encourage adoption.