Skip to content

Commit 6c0f156

Browse files
maorlegerCopilot
andcommitted
feat(typespec-ts-pristine): scaffold north-star emitter package
Three-layer pipeline architecture (adapter → code model → renderer) following the proven patterns from typespec-rust and autorest.go. Contains: - Design document (docs/DESIGN.md) with full surface inventory and IR contract - Code model IR types (src/codemodel/) — pure data, no TCGC imports - Adapter stub (src/tcgcadapter/) — TCGC boundary, only layer that imports TCGC - Renderer stub (src/codegen/) — consumes IR only, one function per file kind - Comparator scaffold (src/comparator/) — diff tool interface for validation - Package infrastructure (package.json, tsconfig.json, README.md) All stubs compile cleanly. No implementation yet — architecture only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 30a8e08 commit 6c0f156

9 files changed

Lines changed: 1024 additions & 0 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# @azure-tools/typespec-ts-pristine
2+
3+
North-star TypeScript emitter for TypeSpec. Clean-room implementation of the
4+
three-layer pipeline architecture proven by `typespec-rust` and `autorest.go`.
5+
6+
## Architecture
7+
8+
```
9+
TCGC SDK Context → Adapter → Code Model (IR) → Renderer → .ts files
10+
```
11+
12+
Three layers, three directories:
13+
14+
| Layer | Directory | Responsibility |
15+
|-------|-----------|----------------|
16+
| Adapter | `src/tcgcadapter/` | Transforms TCGC types into language-specific IR. Only layer that imports TCGC. |
17+
| Code Model | `src/codemodel/` | Pure data types. The contract between adapter and renderer. |
18+
| Renderer | `src/codegen/` | Consumes IR, produces TypeScript source strings. Zero TCGC knowledge. |
19+
20+
## Why does this exist?
21+
22+
The existing `@azure-tools/typespec-ts` emitter grew organically and fuses
23+
adapter and renderer concerns. This package is a greenfield rewrite that
24+
enforces strict layer separation from day one. It targets feature parity with
25+
the existing emitter while being simpler to understand and maintain.
26+
27+
## Comparator
28+
29+
The `compare` script runs both emitters over the same TypeSpec fixture set and
30+
diffs the generated output. It lives in `src/comparator/`.
31+
32+
```bash
33+
# Not yet implemented — interface only
34+
pnpm compare --fixtures ../typespec-test/test/ --baseline ../typespec-ts --candidate .
35+
```
36+
37+
Output: tree diff, per-file unified diff, summary score (% files identical).
38+
39+
## Development
40+
41+
```bash
42+
pnpm install # from repo root
43+
pnpm build # builds this package
44+
pnpm compare # runs comparator (once implemented)
45+
```
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# Design: @azure-tools/typespec-ts-pristine
2+
3+
## What & Why
4+
5+
This package is a clean-room TypeScript emitter for TypeSpec. It follows the
6+
three-layer pipeline architecture proven by `typespec-rust` and `autorest.go`:
7+
8+
```
9+
┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
10+
│ TypeSpec + │ ──▶ │ Adapter │ ──▶ │ Code Model │ ──▶ │ Renderer │ ──▶ .ts files
11+
│ TCGC SDK │ │ (Phase 1) │ │ (IR) │ │ (Phase 3) │
12+
└─────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
13+
```
14+
15+
**Why rewrite?** The existing `@azure-tools/typespec-ts` emitter grew
16+
organically. Adapter and renderer concerns are fused. TCGC types leak into
17+
rendering logic. Symptom-fix dedupe passes accumulate. This package answers:
18+
*"What would we build if we knew all the requirements and started fresh?"*
19+
20+
The answer is boring. Three layers. Each layer has one job. No clever
21+
metaprogramming. The renderer never imports TCGC. The adapter never emits
22+
strings. The code model is the contract.
23+
24+
---
25+
26+
## Surface Area Inventory
27+
28+
The existing emitter produces the following output file categories. Pristine
29+
must cover all of them to achieve parity:
30+
31+
| # | Output Category | Existing Source Location | IR Driver |
32+
|---|----------------|--------------------------|-----------|
33+
| 1 | **Models** (interfaces/types) | `src/modular/emitModels.ts` | `TSModel[]` |
34+
| 2 | **Enums** (type aliases + known values) | `src/modular/emitModels.ts` | `TSEnum[]` |
35+
| 3 | **Unions** (type aliases) | `src/modular/emitModels.ts` | `TSUnion[]` |
36+
| 4 | **Operations** (send/deserialize/public API) | `src/modular/buildOperations.ts`, `src/codegen/operations.ts` | `TSOperation[]` via `TSClient` |
37+
| 5 | **Client context** (factory + interface) | `src/modular/buildClientContext.ts`, `src/codegen/clients.ts` | `TSClient` |
38+
| 6 | **Classical client** (class wrapper) | `src/modular/buildClassicalClient.ts`, `src/codegen/classicalClient.ts` | `TSClient` |
39+
| 7 | **Classical operation groups** | `src/modular/buildClassicalOperationGroups.ts`, `src/codegen/classicalOperations.ts` | `TSOperationGroup[]` |
40+
| 8 | **Options interfaces** | `src/codegen/apiOptions.ts` | `TSOptionsType` per operation |
41+
| 9 | **Serializers** (JSON/XML) | `src/modular/serialization/` | `TSSerializerGroup[]` + `TSModel[]` |
42+
| 10 | **Paging helpers** | `src/modular/static-helpers-metadata.ts` (PagingHelpers) | `TSPagingConfig` |
43+
| 11 | **Polling/LRO helpers** | `src/modular/static-helpers-metadata.ts` (PollingHelpers) | `TSPollingConfig` |
44+
| 12 | **RestorePoller** | `src/modular/buildRestorePoller.ts`, `src/codegen/lroHelpers.ts` | `TSClient.lroConfig` |
45+
| 13 | **Logger** | `src/modular/emitLoggerFile.ts` | `TSGenerationSettings.packageName` |
46+
| 14 | **Index files** (root, subpath, models, api) | `src/modular/buildRootIndex.ts`, `src/modular/buildSubpathIndex.ts`, `src/codegen/indexFiles.ts` | Full `TSCodeModel` |
47+
| 15 | **Package infrastructure** (package.json, tsconfig, etc.) | `src/modular/buildProjectFiles.ts` | `TSGenerationSettings` |
48+
| 16 | **Samples** | `src/modular/emitSamples.ts` | `TSClient` + `TSOperation` |
49+
| 17 | **Tests** | `src/modular/emitTests.ts` | `TSClient` |
50+
| 18 | **Response type aliases** | `src/codegen/responseTypes.ts` | `TSOperation.returnType` |
51+
| 19 | **Static helpers** (URL template, multipart, platform types) | `src/modular/static-helpers-metadata.ts` | `TSHelperFile[]` |
52+
53+
---
54+
55+
## IR Shapes (The Contract)
56+
57+
Each surface is driven by specific IR types. These are defined in
58+
`src/codemodel/index.ts`. Key types:
59+
60+
| IR Type | Drives | Key Fields |
61+
|---------|--------|------------|
62+
| `TSCodeModel` | Root — everything | clients, models, enums, unions, serializers, helpers, settings |
63+
| `TSGenerationSettings` | Package config, infra files | packageName, flavor, isArm, outputDir |
64+
| `TSClient` | Client context + classical class | name, parameters, endpoint, methods, operationGroups, children |
65+
| `TSOperation` | Operation files + options | name, kind, httpMethod, path, parameters, returnType, optionsType |
66+
| `TSOperationGroup` | Grouped operation files | name, operations |
67+
| `TSModel` | Model interfaces + serializers | name, properties, baseModel, discriminator, needsSerializer |
68+
| `TSEnum` | Enum type aliases | name, members, isExtensible, valueType |
69+
| `TSUnion` | Union type aliases | name, variants, discriminator |
70+
| `TSSerializerGroup` | Serializer files | contentType, models |
71+
| `TSHelperFile` | Static helper copies | outputPath, category |
72+
| `TSPagingConfig` | Paging helper inclusion | hasPaging, itemPropertyPath |
73+
| `TSPollingConfig` | LRO helper inclusion | hasLro, emitRestorePoller |
74+
| `TSParameter` | Shared parameter shape | name, type, required, defaultValue |
75+
| `TSProperty` | Model/options properties | name, type, optional, readonly, serializedName |
76+
| `TSDiscriminator` | Polymorphic hierarchies | propertyName, value, variants |
77+
| `TSOptionsType` | Per-operation options bag | name, properties |
78+
| `TSEndpoint` | Client endpoint config | urlTemplate, isParameterized, templateParams |
79+
| `TSApiVersion` | API versioning | paramName, defaultValue, isInEndpoint |
80+
81+
---
82+
83+
## Non-Negotiable Invariants
84+
85+
1. **Renderer does not import TCGC.** Not transitively, not via re-export, not
86+
via a "utils" file that sneaks it in. If the renderer needs data, it goes
87+
in the code model.
88+
89+
2. **Code model is the contract.** The adapter's output type is `TSCodeModel`.
90+
The renderer's input type is `TSCodeModel`. That's the only coupling.
91+
92+
3. **No symptom-fix dedupe passes.** If duplicate imports appear, the adapter
93+
is producing bad data. Fix the adapter. Don't add a post-processing strip
94+
pass.
95+
96+
4. **No clever metaprogramming.** No code that generates code that generates
97+
code. String builders or template literals, same as Go and Rust.
98+
99+
5. **File-per-concern.** Each renderer function produces one logical file kind.
100+
No 500-line functions that emit three different file types.
101+
102+
6. **Pure data code model.** No methods on IR types. No side effects. No
103+
closures. Serializable to JSON.
104+
105+
7. **Self-contained. No internal workspace dependencies. Always extractable.**
106+
This package has ZERO dependencies on other packages in this monorepo — not
107+
`@azure-tools/rlc-common`, not `@azure-tools/typespec-ts`, nothing. The
108+
only allowed dependencies are external npm packages (`@typespec/*`,
109+
`@azure-tools/typespec-client-generator-core`, `ts-morph`, `tslib`, etc.).
110+
If a utility exists in a sibling package and we need it, we copy it in
111+
(with attribution). The package must be liftable to its own repo at any
112+
time: `cp -r packages/typespec-ts-pristine ../new-repo/ && npm install`
113+
must work.
114+
115+
---
116+
117+
## Explicit Non-Goals
118+
119+
- **AutoRest parity.** The `autorest.typescript` package is in maintenance
120+
mode. Pristine targets TypeSpec-only generation.
121+
122+
- **Experimental flags.** No `enableExperimentalFeature` toggles. Features are
123+
either implemented or they aren't.
124+
125+
- **Legacy customer overlays.** No hooks for customers to patch generated code
126+
inside the emitter. That's a migration concern, not a design concern.
127+
128+
- **RLC generation.** Pristine generates modular SDK only. RLC is handled by
129+
the existing emitter in maintenance mode, or by a separate focused package.
130+
131+
---
132+
133+
## Comparator Approach
134+
135+
A `compare` script validates pristine output against the existing emitter.
136+
137+
### Interface
138+
139+
```
140+
compare(fixturesDir, baselineOutput, candidateOutput) → CompareResult
141+
```
142+
143+
### Location
144+
145+
`src/comparator/index.ts` — types and orchestration logic.
146+
`pnpm compare` — CLI entry (package.json script).
147+
148+
### What it does
149+
150+
1. Enumerates fixture directories under `packages/typespec-test/test/`
151+
2. For each fixture, globs all `.ts` files in both output trees
152+
3. Computes: files only in baseline, files only in candidate, files with diffs
153+
4. Produces per-file unified diffs
154+
5. Calculates a score: `(identical files / total files) × 100`
155+
156+
### Output format
157+
158+
```
159+
Fixture: azure/storage-blob
160+
Score: 94.2% (47/50 files identical)
161+
Missing in candidate: src/models/legacy.ts
162+
Extra in candidate: (none)
163+
Diffs:
164+
src/api/containers/operations.ts (12 lines changed)
165+
src/index.ts (3 lines changed)
166+
```
167+
168+
### What it does NOT do
169+
170+
- Does not compile the output (that's the smoke test's job)
171+
- Does not run integration tests (that's the integration suite's job)
172+
- Does not evaluate which output is "better" — only equivalence
173+
- Does not import the baseline emitter as a dependency — it either invokes it
174+
as a subprocess (`npx @azure-tools/typespec-ts`) or diffs pre-generated
175+
output that already exists in `packages/typespec-test/test/*/generated/`
176+
177+
---
178+
179+
## Directory Structure
180+
181+
```
182+
packages/typespec-ts-pristine/
183+
├── package.json
184+
├── tsconfig.json
185+
├── README.md
186+
├── docs/
187+
│ └── DESIGN.md ← this file
188+
└── src/
189+
├── index.ts ← emitter entry point (3-phase orchestrator)
190+
├── tcgcadapter/
191+
│ └── index.ts ← TCGC → TSCodeModel transformation
192+
├── codemodel/
193+
│ └── index.ts ← IR type definitions (pure data)
194+
├── codegen/
195+
│ └── index.ts ← TSCodeModel → .ts file strings
196+
└── comparator/
197+
└── index.ts ← diff tool for validating output equivalence
198+
```
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "@azure-tools/typespec-ts-pristine",
3+
"version": "0.1.0",
4+
"description": "North-star TypeScript emitter for TypeSpec — clean three-layer pipeline architecture",
5+
"main": "dist/src/index.js",
6+
"type": "module",
7+
"exports": {
8+
".": {
9+
"default": "./dist/src/index.js",
10+
"types": "./dist/src/index.d.ts"
11+
}
12+
},
13+
"scripts": {
14+
"build": "tsc -p .",
15+
"clean": "rimraf ./dist",
16+
"compare": "echo 'comparator not yet implemented' && exit 1"
17+
},
18+
"author": "Microsoft Corporation",
19+
"license": "MIT",
20+
"devDependencies": {
21+
"@azure-tools/typespec-client-generator-core": "^0.68.0",
22+
"@typespec/compiler": "^1.12.0",
23+
"@typespec/http": "^1.12.0",
24+
"@typespec/rest": "^0.82.0",
25+
"@typespec/versioning": "^0.82.0",
26+
"typescript": "~5.6.3"
27+
},
28+
"peerDependencies": {
29+
"@azure-tools/typespec-client-generator-core": "^0.68.0",
30+
"@typespec/compiler": "^1.12.0"
31+
},
32+
"dependencies": {
33+
"tslib": "^2.3.1"
34+
}
35+
}

0 commit comments

Comments
 (0)