Repository navigation
Add emitter-diff: language-agnostic cross-version generated-code diffing - #4784
Libba Lawrence (l0lawrence) wants to merge 18 commits into
Conversation
Adds eng/emitter-diff, a language-agnostic CLI that generates code with two emitter versions (npm/local/github refs) and diffs the output (terminal, VS Code, or HTML). Includes a typespec-python adapter wrapping regenerate.ts and run-tests.ts, a python PR CI workflow, and --emitter-dir/--output-dir/ --http-specs-dir/--azure-specs-dir/--use-pyodide overrides on regenerate.ts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
VS Code has no folder-diff CLI, so --open now stages the comparison as a throwaway git working tree (<work-dir>/vscode-diff): baseline is committed and head is overlaid + staged, so the Source Control panel shows every changed generated file with side-by-side diffs. Documents the flag and the equivalent manual command in the README. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
--patch <file> writes the raw unified diff to a file (printing only the summary to the terminal) instead of dumping the full patch. Also build a single quoted command line for shell-routed spawns so Node's DEP0190 (args array + shell:true) warning no longer prints when launching code/npm shims on Windows. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ide default for python on Windows Output presentation: - Default output is now a clickable HTML report written to <work-dir>/emitter-diff.html, with a file:// link printed to the terminal. - --vscode opens the live VS Code source diff (--open kept as an alias). - --terminal restores the full colored patch dump; --patch/--html still target files. Python adapter: - Default --use-pyodide on Windows (native black fails under deep pnpm venv paths) and off elsewhere (Linux CI native is faster). Override with --opt pyodide=true|false. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
openInVsCode committed the baseline tree then overlaid head and staged with a plain 'git add -A'. Git decides a file is unchanged from (size, mtime) and skips re-hashing; since the baseline commit and head overlay are written in the same second, a same-size content edit was treated as clean and never staged, so it silently did not appear in VS Code (adds/deletes/size-changing edits still did). Clear the index (git rm -r --cached) before re-adding so git re-hashes every file's content and surfaces every real modification. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The python emitter-diff workflow already uploaded the rendered HTML as a per-PR artifact and wrote a job summary; add a sticky PR comment (updated in place via a hidden marker) that lists the changed files and +/- counts and links to the artifact download. Also emit --patch so the comment parses exact file/line counts straight from the unified diff, grant pull-requests: write, and mark the HTML upload if-no-files-found: ignore for the no-diff case. Fork PRs get a read-only token so the comment is best-effort (continue-on-error). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…diff-test-tool # Conflicts: # packages/typespec-python/eng/scripts/ci/regenerate-common.ts # pnpm-lock.yaml
Upstream PR #4399 moved/rewrote the python regenerate driver to eng/scripts/ci/regenerate.ts (in-process compilation). Re-apply the emitter-diff overrides on the new wrapper and point the adapter at it: - regenerate.ts: add --httpSpecsDir/--azureSpecsDir/--use-pyodide/--no-baseline options; thread use-pyodide into emitter task options; gate the azure-sdk-for-python baseline clone behind --no-baseline. All overrides live in the non-synced wrapper so regenerate-common.ts stays identical upstream. - python adapter: drive eng/scripts/ci/regenerate.ts with --pluginDir (emitter build) + --generatedFolder (output root, anchored so files land in <outputDir>/tests/generated); pin specs to the current checkout so only the emitter varies; pass --no-baseline. Update runTests sync source for the new nested layout. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
❌ There is undocummented changes. Run The following packages have changes but are not documented.
The following packages have already been documented:
Show changes
|
Python emitter diffBaseline No changes to generated output. Full rendered diff: emitter-diff-html artifact on the run. Informational check (eng/emitter-diff); does not block the PR. |
The eng/emitter-diff sources imported sibling modules with explicit .ts extensions, which only typechecks under allowImportingTsExtensions. The repo-wide check:eng (tsc -p tsconfig.eng.json) compiles ./eng without that flag, so the build failed with TS5097. Switch to the repo's standard convention instead of enabling the flag: - import sibling modules with .js specifiers (NodeNext resolves them to the .ts sources during typecheck; tsx/esbuild resolves them at runtime). - drop allowImportingTsExtensions from the package tsconfig. - run the CLI via 'npx tsx' (workflow, README, shebang) since node's type-stripping does not remap .js to .ts. Runtime script-path arguments (eng/scripts/ci/regenerate.ts etc.) are left as .ts since they are passed to tsx, not imported. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- cspell: allow 'gpgsign' (a git config key used in diff.ts overlay). - versions consistency: move diff2html to the pnpm catalog instead of a pinned version; add diff2html (^3.4.48) to the workspace catalog and reference it via 'catalog:' from eng/emitter-diff, updating the lockfile. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
commit: |
|
You can try these changes here
|
⚡ Benchmark Results
Full details – comparing
|
| Metric | Baseline | Current | Change |
|---|---|---|---|
| total | 🔴 729.1ms | 🔴 1.06s | +45.2% 🔴 |
| loader | 🟡 209.3ms | 🟡 326.7ms | +56.1% 🔴 |
| resolver | 🟢 23.1ms | 🟢 41.0ms | +77.8% 🔴 |
| checker | 🟡 235.2ms | 🟡 379.4ms | +61.3% 🔴 |
| validation | 🟢 53.9ms | 🟢 87.4ms | +62.1% 🔴 |
| ↳ validation/@azure-tools/typespec-azure-core | 🟢 7.9ms | 🟡 13.1ms | +66.9% 🔴 |
| ↳ validation/@typespec/http | 🟢 7.9ms | 🟡 16.2ms | +105.7% 🔴 |
| ↳ validation/@typespec/rest | 🟢 0.8ms | 🟢 1.7ms | +100.6% |
| ↳ validation/@typespec/versioning | 🔴 34.7ms | 🔴 52.3ms | +51.0% 🔴 |
| ↳ validation/compiler | 🟢 2.2ms | 🟢 3.9ms | +81.1% 🔴 |
| linter | 🟢 161.0ms | 🟡 223.6ms | +38.9% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/auth-required | 🟢 0.0ms | 🟢 0.1ms | +200.7% |
| ↳ linter/@azure-tools/typespec-azure-core/bad-record-type | 🟢 0.3ms | 🟢 0.6ms | +94.7% |
| ↳ linter/@azure-tools/typespec-azure-core/byos | 🟢 6.6ms | 🟢 8.5ms | +29.7% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/casing-style | 🟢 0.8ms | 🟢 1.5ms | +84.6% |
| ↳ linter/@azure-tools/typespec-azure-core/composition-over-inheritance | 🟢 0.1ms | 🟢 0.2ms | +97.4% |
| ↳ linter/@azure-tools/typespec-azure-core/documentation-required | 🟢 1.1ms | 🟢 2.0ms | +82.6% |
| ↳ linter/@azure-tools/typespec-azure-core/friendly-name | 🟢 0.8ms | 🟢 1.4ms | +81.5% |
| ↳ linter/@azure-tools/typespec-azure-core/key-visibility-required | 🟢 0.2ms | 🟢 0.4ms | +82.3% |
| ↳ linter/@azure-tools/typespec-azure-core/known-encoding | 🟢 0.3ms | 🟢 0.5ms | +54.3% |
| ↳ linter/@azure-tools/typespec-azure-core/long-running-polling-operation-required | 🟢 0.4ms | 🟢 0.8ms | +103.7% |
| ↳ linter/@azure-tools/typespec-azure-core/no-case-mismatch | 🟢 0.3ms | 🟢 0.7ms | +108.0% |
| ↳ linter/@azure-tools/typespec-azure-core/no-closed-literal-union | 🟢 0.4ms | 🟢 1.0ms | +131.8% |
| ↳ linter/@azure-tools/typespec-azure-core/no-enum | 🟢 0.1ms | 🟢 0.3ms | +191.9% |
| ↳ linter/@azure-tools/typespec-azure-core/no-error-status-codes | 🟢 0.1ms | 🟢 0.3ms | +106.8% |
| ↳ linter/@azure-tools/typespec-azure-core/no-explicit-routes-resource-ops | 🟢 0.1ms | 🟢 0.2ms | +55.3% |
| ↳ linter/@azure-tools/typespec-azure-core/no-format | 🟢 0.6ms | 🟢 0.8ms | +33.1% |
| ↳ linter/@azure-tools/typespec-azure-core/no-generic-numeric | 🟢 0.5ms | 🟢 0.8ms | +59.5% |
| ↳ linter/@azure-tools/typespec-azure-core/no-header-explode | 🔴 21.3ms | 🔴 28.2ms | +32.2% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/no-legacy-usage | 🟢 1.3ms | 🟢 2.0ms | +53.1% |
| ↳ linter/@azure-tools/typespec-azure-core/no-multiple-discriminator | 🟢 0.1ms | 🟢 0.3ms | +120.0% |
| ↳ linter/@azure-tools/typespec-azure-core/no-nullable | 🟢 0.3ms | 🟢 0.4ms | +47.2% |
| ↳ linter/@azure-tools/typespec-azure-core/no-offsetdatetime | 🟢 1.4ms | 🟢 2.1ms | +49.6% |
| ↳ linter/@azure-tools/typespec-azure-core/no-openapi | 🟢 2.1ms | 🟢 2.3ms | +11.7% |
| ↳ linter/@azure-tools/typespec-azure-core/no-private-usage | 🟢 2.2ms | 🟢 3.1ms | +40.4% |
| ↳ linter/@azure-tools/typespec-azure-core/no-query-explode | 🔴 22.6ms | 🔴 29.2ms | +29.2% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/no-response-body | 🔴 26.5ms | 🔴 34.9ms | +31.7% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/no-rest-library-interfaces | 🟢 0.0ms | 🟢 0.1ms | +178.7% |
| ↳ linter/@azure-tools/typespec-azure-core/no-route-parameter-name-mismatch | 🟢 5.8ms | 🟢 8.1ms | +40.8% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/no-rpc-path-params | 🟢 0.2ms | 🟢 0.4ms | +73.5% |
| ↳ linter/@azure-tools/typespec-azure-core/no-string-discriminator | 🟢 0.1ms | 🟢 0.2ms | +129.0% |
| ↳ linter/@azure-tools/typespec-azure-core/no-unknown | 🟢 0.2ms | 🟢 0.4ms | +57.0% |
| ↳ linter/@azure-tools/typespec-azure-core/no-unnamed-union | 🟢 0.5ms | 🟢 0.8ms | +76.0% |
| ↳ linter/@azure-tools/typespec-azure-core/operation-missing-api-version | 🟢 0.2ms | 🟢 0.4ms | +67.5% |
| ↳ linter/@azure-tools/typespec-azure-core/request-body-problem | 🟢 0.3ms | 🟢 0.5ms | +36.5% |
| ↳ linter/@azure-tools/typespec-azure-core/require-versioned | 🟢 0.0ms | 🟢 0.1ms | +228.4% |
| ↳ linter/@azure-tools/typespec-azure-core/response-schema-problem | 🔴 24.9ms | 🔴 34.5ms | +38.7% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/rpc-operation-request-body | 🟢 0.4ms | 🟢 0.7ms | +86.0% |
| ↳ linter/@azure-tools/typespec-azure-core/spread-discriminated-model | 🟢 0.3ms | 🟢 0.5ms | +51.7% |
| ↳ linter/@azure-tools/typespec-azure-core/use-standard-names | 🟢 6.1ms | 🟢 7.9ms | +29.6% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/use-standard-operations | 🟢 0.2ms | 🟢 0.3ms | +66.5% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-agent-base-type-child-resources | 🟢 7.5ms | 🔴 21.1ms | +180.3% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-agent-base-type-lifecycle-operations | 🟢 0.1ms | 🟢 0.2ms | +330.6% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-common-types-version | 🟢 5.9ms | 🟡 12.2ms | +107.4% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-custom-resource-no-key | 🟢 0.1ms | 🟢 0.2ms | +104.5% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-custom-resource-usage-discourage | 🟢 0.1ms | 🟢 0.1ms | +76.4% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-delete-operation-response-codes | 🟢 2.3ms | 🟢 4.5ms | +99.9% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-no-path-casing-conflicts | 🟡 12.6ms | 🟡 16.8ms | +34.2% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-no-record | 🟢 0.4ms | 🟢 0.7ms | +62.0% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-post-operation-response-codes | 🟢 0.7ms | 🟢 1.4ms | +101.6% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-put-operation-response-codes | 🟢 0.1ms | 🟢 0.2ms | +240.6% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-action-no-segment | 🟢 0.3ms | 🟢 0.5ms | +67.0% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-duplicate-property | 🟢 0.2ms | 🟢 0.4ms | +132.5% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-interface-requires-decorator | 🟢 0.0ms | 🟢 0.1ms | +200.3% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-action-verb | 🟢 0.1ms | 🟢 0.2ms | +70.0% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-envelope-property | 🟢 0.1ms | 🟢 0.3ms | +116.7% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-version-format | 🟢 0.1ms | 🟢 0.2ms | +164.6% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-key-invalid-chars | 🟢 0.3ms | 🟢 0.5ms | +78.0% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-name-pattern | 🟢 0.0ms | 🟢 0.1ms | +276.3% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-operation | 🟢 0.2ms | 🟢 0.6ms | +138.7% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-operation-response | 🟢 5.9ms | 🟢 9.5ms | +61.4% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-patch | 🟢 0.4ms | 🟢 0.9ms | +102.6% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-path-segment-invalid-chars | 🟢 0.2ms | 🟢 0.4ms | +66.7% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-provisioning-state | 🟢 0.2ms | 🟢 0.4ms | +147.8% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/beyond-nesting-levels | 🟢 0.1ms | 🟢 0.2ms | +112.5% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/empty-updateable-properties | 🟢 0.2ms | 🟢 0.4ms | +109.6% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/improper-subscription-list-operation | 🟢 0.0ms | 🟢 0.1ms | +321.1% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/lro-location-header | 🟡 15.4ms | 🔴 21.1ms | +37.3% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/missing-operations-endpoint | 🟢 0.0ms | 🟢 0.2ms | +204.3% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/missing-x-ms-identifiers | 🟢 0.4ms | 🟢 1.0ms | +128.8% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/no-empty-model | 🟢 0.2ms | 🟢 0.3ms | +77.4% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/no-override-props | 🟢 0.1ms | 🟢 0.3ms | +165.8% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/no-resource-delete-operation | 🟢 0.2ms | 🟢 0.5ms | +101.3% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/no-response-body | 🔴 22.6ms | 🔴 32.4ms | +43.2% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/patch-envelope | 🟢 0.2ms | 🟢 0.4ms | +110.3% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/resource-name | 🟢 0.2ms | 🟢 0.5ms | +118.8% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/secret-prop | 🟢 4.5ms | 🟢 5.3ms | +18.6% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/unsupported-type | 🟢 0.4ms | 🟢 0.7ms | +60.1% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/version-progression | 🟢 0.1ms | 🟢 0.3ms | +231.9% |
| ↳ linter/@azure-tools/typespec-client-generator-core/property-name-conflict | 🟢 1.4ms | 🟢 2.4ms | +74.3% 🔴 |
| ↳ linter/@azure-tools/typespec-client-generator-core/require-client-suffix | 🟢 0.5ms | 🟢 1.7ms | +228.4% 🔴 |
| emit | 🔴 5.63s | 🔴 6.20s | +10.1% 🔴 |
| ↳ emit/Azure-Tools | 🟢 0.0ms | 🟢 0.0ms | +0.0% |
| ↳ emit/@azure-tools/typespec-autorest | 🟢 91.0ms | 🟢 127.8ms | +40.5% 🔴 |
| ↳ emit/@azure-tools/typespec-python | 🔴 2.15s | 🔴 2.31s | +7.4% 🔴 |
| ↳ emit/typespec | 🟢 0.0ms | 🟢 0.0ms | +0.0% |
| ↳ emit/@typespec/http-client-js | 🔴 487.7ms | 🔴 589.5ms | +20.9% 🔴 |
| ↳ emit/@typespec/openapi3 | 🟢 81.8ms | 🟢 111.0ms | +35.7% 🔴 |
| ↳ emit/@typespec/openapi3/compute | 🟢 71.3ms | 🟢 96.3ms | +35.1% 🔴 |
| ↳ emit/@typespec/openapi3/write | 🟢 10.3ms | 🟢 14.4ms | +39.6% 🔴 |
Averaged across 3 specs (azure-arm-resource-manager, azure-core-dataplane, azure-full).
Threshold: changes > ±5% are highlighted.
🟢 Fast · 🟡 Moderate (stages >200ms, rules >10ms) · 🔴 Slow (stages >400ms, rules >20ms)
- Absolute work-dir fix, --specs fast-fail, parallelization + logPrefix (from core) - Remove --vscode/openInVsCode (HTML report is sufficient) - Workflow: use PIPESTATUS for tool exit code, drop per-file list (link HTML), add hard-error PR comment branch Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mirrors the core emitter-diff improvements to the Azure workflow: - Baseline is now the PR's git merge-base with its base branch (survives squash-merge / rebase / force-push) instead of the latest npm version. - The check is informational: it never fails the PR, only reports the generated-output diff via job summary, PR comment, and HTML artifact. - Security hardening in resolver.ts (validate git refs/repos, --end-of-options, npm install --ignore-scripts) and de-duplicated HTML report in diff.ts. - Removed emojis from tool output, job summary, and PR comment. - Moved untrusted workflow inputs into env vars (Actions expression injection). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds a "Fail on tool error" step so the informational check fails only when the tool hits a real build/venv/generate error (non-zero exit), not on a diff. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Trim the tool down to plain TypeScript scripts with no runtime dependencies and no package wrapper. - Replace diff2html with a self-contained, GitHub-style HTML renderer (inline CSS, light/dark, no external requests); remove it from the package deps, pnpm catalog, and lockfile. - Drop the --run-tests/--test-env/--test-target surface and the adapter runTests contract; parallel generation and --sequential are unchanged. - Delete package.json and drop the tool from pnpm-workspace.yaml so it is no longer a named package (removes npm dependency-confusion surface). - Run with plain ode (Node 24 strips TypeScript natively) instead of tsx: rewrite relative imports to .ts and set allowImportingTsExtensions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Running emitter-diff with plain node requires .ts import specifiers, but the repo-wide check:eng gate (tsconfig.eng.json, --noEmit) rejected them. Enable allowImportingTsExtensions there so the tool stays covered by the existing typecheck. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A local run now prints the exact generated trees the diff compares, so developers can open the head (current checkout) output directly instead of inferring it from the work dir. Language-agnostic: lives in cli.ts, labeled per side by the resolved emitter. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add a typescript adapter (@azure-tools/typespec-ts) and a rust adapter (@azure-tools/typespec-rust) to the language-agnostic emitter-diff tool, plus a typescript CI workflow mirroring the python one. - typescript adapter wraps the package's gen-spector.js driver; adds --emitter-dir/--output-dir override hooks to run.ts + gen-spector.js so any emitter build can target an isolated output dir. - rust adapter wraps .scripts/tspcompile.js; inert-until-present (fails fast with guidance) since typespec-rust has not moved into this repo yet. - ci-emitter-diff-typescript.yml: merge-base baseline, informational-only PR comment + HTML artifact. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
TypeScript emitter diffBaseline No changes to generated output. Full rendered diff: emitter-diff-html-typescript artifact on the run. Informational check (eng/emitter-diff); does not block the PR. |
Cut the python + typescript workflows to ~half their size: - trim explanatory comment headers - drop the hand-rolled github-script patch parser; reuse the tool's own 'Diff summary:' line (ANSI-stripped) as a step output for the PR comment - drop the redundant job-summary step and the now-unused --patch output - collapse 'Fail on tool error' to a one-liner Behavior is unchanged: sticky PR comment, HTML artifact, informational-only (fails only on a tool/build error, never on a diff). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Summary
Adds
eng/emitter-diff, a language-agnostic CLI for diffing the generated-code output of two TypeSpec emitter versions (baseline vs. head), plus CI workflows that surface the diff on PRs. The goal is a shared way for every language emitter that lives (or will live) in this repo to regenerate their code-gen tests and review cross-version diffs — locally and in CI.Each emitter version can come from a published npm version, a local folder, or a GitHub url/sha. The tool drives each emitter's own generate command (it never reimplements codegen), diffs the two outputs, and renders the result as HTML, a terminal patch, or a
.patchfile. Fortypespec-pythonthis can replacenpm run regeneratefor diffing purposes (both are kept for now).What's included
Core tool (
eng/emitter-diff/) — zero runtime deps (Node built-ins run the.tsdirectly; the HTML renderer is dependency-free). A pluggableEmitterAdaptercontract keeps all language specifics behind per-language adapters; the CLI itself contains zero language logic. Baseline/head/specs refs are resolved uniformly (npm:/local:/github:/gh:).Adapters (
eng/emitter-diff/src/adapters/):@azure-tools/typespec-python) — wraps the package's ownregenerate.ts, driven with--pluginDir(emitter build) +--generatedFolder(isolated output root).@azure-tools/typespec-ts) — wraps the package'stest/commands/gen-spector.js; adds--emitter-dir/--output-diroverride hooks torun.ts+gen-spector.jsso any emitter build can target an isolated output dir. Runs only theclientphase (the generated sources the diff compares) and stages specs once viacopy:typespec.@azure-tools/typespec-rust) — inert-until-present:typespec-rusthasn't moved into this repo yet, so the adapter fails fast with guidance. It's ready to activate once the package lands and its.scripts/tspcompile.jsgains the same--emitter-dir/--output-dirhooks (documented in the adapter).Output modes — default writes a clickable HTML report +
file://link;--terminalprints the colored patch;--patch <file>/--html <file>write to disk;--fail-on-diffgates in CI (exit2= diff present,1= hard error).--namescopes which specs generate; parallel by default,--sequentialto serialize.CI — one informational workflow per language:
.github/workflows/ci-emitter-diff-python.yml.github/workflows/ci-emitter-diff-typescript.ymlOn a matching PR each builds the emitter, diffs the current checkout against the merge-base baseline (the base-branch commit the PR is based on — survives squash/rebase, unlike a pinned SHA), uploads the rendered-diff artifact, and posts a sticky PR comment reusing the tool's own
Diff summary:line. These checks are informational: they never fail on a diff, only on a real tool/build error.Verification
✓ No differences.npm:0.54.2vsnpm:0.55.0-dev.8,--name routes) →5 file(s), +2 / -28— correctly captured the emitter dropping the emptymodelsmodule and bumping the node engine>=20→>=22. HTML + patch rendered.packages/typespec-rustis absent.Notes
@azure-tools/typespec-python/-ts/-rust); the microsoft/typespec copy targets@typespec/http-client-python. The typescript + rust adapters are Azure-only.--opt pyodide=truewhere a native toolchain is unavailable.Opening as draft for feedback on the approach and adapter contract.