|
| 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 | +``` |
0 commit comments