Skip to content

Add @rushstack/heft-zod-schema-plugin and pilot it on rush-lib's experiments.json#5788

Draft
Copilot wants to merge 9 commits intomainfrom
copilot/replace-json-schema-with-zod
Draft

Add @rushstack/heft-zod-schema-plugin and pilot it on rush-lib's experiments.json#5788
Copilot wants to merge 9 commits intomainfrom
copilot/replace-json-schema-with-zod

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 19, 2026

Address review feedback on the prior pivot to @rushstack/rush-schemas, then take a first-draft pass at the four "next steps" follow-ups (use the other ported schemas in rush-lib, drop the duplicate .schema.json artifacts, validate via zod instead of JsonSchema, and add zod support to JsonFile).

Round 1 — review feedback

  • Move @rushstack/heft-zod-schema-plugin and @rushstack/rush-schemas from browser-approved-packages.json to nonbrowser-approved-packages.json
  • Delete the placeholder CHANGELOG.json / CHANGELOG.md in both new packages (autogenerated at publish)
  • Drop the unnecessary target: "ES2019" override in libraries/rush-schemas/tsconfig.json
  • Place @rushstack/rush-schemas under the rush version policy in rush.json
  • Remove the unused IRepoStateJson type alias from repo-state.zod.ts (rush-lib has its own internal interface)
  • Move @rushstack/rush-schemas to its alphabetical position in libraries/rush-lib/package.json's dependencies
  • Import IExperimentsJson from the package entrypoint (@rushstack/rush-schemas)
  • Test cleanup: use template strings with /s and pass absolute paths in ZodSchemaGenerator.test.ts; harden the generator to accept absolute paths in both inputGlobs and outputFolder
  • Add @rushstack/rush-schemas as a dependency of rush-sdk so its bundled dist/rush-lib.d.ts (which re-exports IExperimentsJson) resolves for downstream consumers

Round 2 — first-draft of the 4 next-steps items

  1. node-core-library zod support on JsonFile — added a structural IJsonFileTypeValidator<T> interface (any object with parse(input: unknown): T, which is the contract zod 3, zod 4, and most other validation libraries already match) and new JsonFile.loadAndParse / loadAndParseAsync helpers. No runtime dependency on a specific zod major version.
  2. Use the other ported schemas in rush-libExperimentsConfiguration, CobuildConfiguration, and RepoStateFile now validate the loaded JSON against experimentsSchema / cobuildSchema / repoStateSchema from @rushstack/rush-schemas (replacing the legacy JsonSchema.fromLoadedObject(...) + JsonFile.loadAndValidate(...) plumbing). ICobuildJson is now re-exported from @rushstack/rush-schemas instead of being duplicated as a separate @beta interface in rush-lib.
  3. Replace the .schema.json files in rush-lib's output — removed the now-unused experiments.schema.json, cobuild.schema.json, and repo-state.schema.json from libraries/rush-lib/src/schemas/. They were only consumed via TS JSON imports inside the three migrated configuration classes; with the runtime path on zod, the bundled .schema.json artifact in rush-lib's output is no longer needed for these three.
  4. build-cache deferred — intentionally left on the legacy path. As called out in libraries/rush-schemas/README.md, the JSON Schema's discriminated oneOf shape and the runtime IBuildCacheJson extension-point shape don't match structurally, and reconciling them needs a focused design discussion (drop the open [key: string]: JsonObject index? hand-author the runtime interface? add a relaxed runtime variant of the schema?).

Round 3 — review feedback follow-ups

  • Derive IExperimentsJson from experimentsSchema via Omit<z.infer<typeof experimentsSchema>, '$schema'> instead of hand-authoring the interface and using a bidirectional drift-check assertion. Per-property documentation now lives on the schema's .describe() annotations as the single source of truth, and the as IExperimentsJson cast in ExperimentsConfiguration.ts drops out cleanly. Updated cobuild.zod.ts cross-reference and libraries/rush-schemas/README.md to remove the now-obsolete reference to the hand-authored-interface pattern (the table collapses to the single z.infer-based derivation, with build-cache called out as the structural-mismatch escape hatch).

Validation

  • rush build --to @microsoft/rush-lib ✅, --to @rushstack/rush-sdk ✅, --to @rushstack/mcp-server ✅, --to @rushstack/node-core-library ✅, --to @rushstack/heft-zod-schema-plugin
  • rush-lib tests: 627/627 ✅
  • node-core-library tests: 267/267 ✅
  • heft-zod-schema-plugin tests: 9/9 ✅
  • API report regenerated; the only changes are ICobuildJson becoming a re-export from @rushstack/rush-schemas and IExperimentsJson switching from a type-only to a value+type re-export. Interface shapes are unchanged for downstream consumers.

Notes / known limitations of this first draft

  • After the round-3 switch to z.infer-derived types, per-property TSDoc on IExperimentsJson no longer renders in IDE tooltips; only the schema's .describe() text is preserved (and that flows into the emitted experiments.schema.json). Interface-level TSDoc is still present.
  • node-core-library does not get a unit test specifically for loadAndParse in this commit — it's a thin wrapper around JsonFile.load + validator.parse, and is exercised end-to-end by the rush-lib test suite. Easy to add a focused unit test if preferred.
  • The remaining ~15 schemas in libraries/rush-lib/src/schemas/ (rush.schema.json, common-versions.schema.json, etc.) are still on the legacy JsonSchema-based path. The pattern established here (schemaName.zod.ts in @rushstack/rush-schemas + JsonFile.loadAndParse(...) in rush-lib + delete the corresponding src/schemas/*.schema.json) is now repeatable per-schema.

Copilot AI and others added 2 commits April 19, 2026 03:49
Copy link
Copy Markdown
Member

@iclanton iclanton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - next steps:

  1. Make use of the other ported schemas in rush-lib
  2. Replace the .schema.json files in rush-lib's output with the ones from the schemas project
  3. Use zod to validate the loaded objects instead of JsonSchema
  4. Consider adding support for zod schemas in node-core-library's JsonFile.

"name": "@reduxjs/toolkit",
"allowedCategories": [ "libraries", "vscode-extensions" ]
},
{
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - these should go in nonbrowser-approved-packages.json

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved both @rushstack/heft-zod-schema-plugin and @rushstack/rush-schemas into nonbrowser-approved-packages.json in 60d69b0.

// rush-mcp-server pins to zod 3 to remain compatible with @modelcontextprotocol/sdk;
// heft-zod-schema-plugin and the rush-lib pilot use zod 4 for its built-in
// z.toJSONSchema() API.
"~3.25.76"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - can we upgrade @modelcontextprotocol/sdk to pull in v4 of zod in the project that currently needs v3?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latest @modelcontextprotocol/sdk (1.25+/1.29) does support zod ^3.25 || ^4.0, so in principle yes. I tried the upgrade locally and it surfaces two unrelated breaks that need real source changes in rush-mcp-server:

  1. apps/rush-mcp-server/src/tools/base.tool.ts uses z.objectOutputType<...> — that's a zod-3 internal type that was removed in zod 4; the BaseTool generic plumbing needs to be re-expressed.
  2. The newer SDK pulls in a transitive transport.d.ts that references the DOM HeadersInit global, which doesn't resolve under the current @types/node / lib settings.

Neither is conceptually hard, but both are out-of-scope for this PR (which is the schemas package pivot) and want their own validation. Reverted the speculative upgrade in this branch and left a follow-up task in the PR description so the zod-3 alternative version stays scoped to rush-mcp-server for now.

it('emits a JSON schema for a basic zod default export', async () => {
const generator: ZodSchemaGenerator = new ZodSchemaGenerator({
buildFolderPath: projectFolder,
inputGlobs: [path.relative(projectFolder, path.join(compiledFixturesFolder, 'basic.zod.js'))],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - Seems like inputGlobs should be able to accept absolute paths. Also, just use template strings with /s.

Same in other places in this file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched the test to template strings with / and absolute paths in 60d69b0. Also hardened ZodSchemaGenerator itself: inputGlobs are now run through Path.convertToSlashes() (so absolute/Windows-style paths reach fast-glob in its required form), and outputFolder is resolved with path.resolve() instead of path.join() so an absolute value works as expected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - delete this file (and CHANGELOG.md). They will be autogenerated during publish.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted CHANGELOG.json and CHANGELOG.md in 60d69b0.

Comment thread libraries/rush-schemas/CHANGELOG.json Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - same here as the other new CHANGELOG.* files.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted both files in 60d69b0.

Comment thread libraries/rush-schemas/tsconfig.json Outdated
"extends": "./node_modules/local-node-rig/profiles/default/tsconfig-base.json",

"compilerOptions": {
"target": "ES2019"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No good reason — leftover from copying another project's tsconfig. The rig already sets a sensible target. Removed the override in 60d69b0.

Comment thread rush.json
"projectFolder": "libraries/rush-schemas",
"reviewCategory": "libraries",
"shouldPublish": true
},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in the Rush version policy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to "versionPolicyName": "rush" in 60d69b0.

*/
export type IRepoStateJson = Omit<z.infer<typeof repoStateSchema>, '$schema'>;

export default repoStateSchema;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are unnecessary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the IRepoStateJson type alias (and its index.ts re-export) in 60d69b0. rush-lib's RepoStateFile.ts already keeps its own internal interface, and nothing else consumes it.

Comment thread libraries/rush-lib/package.json Outdated
"pnpm-sync-lib": "0.3.3",
"read-package-tree": "~5.1.5",
"rxjs": "~6.6.7",
"@rushstack/rush-schemas": "workspace:*",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - put this in alphabetical order.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved @rushstack/rush-schemas to its alphabetical position among the other @rushstack/* workspace deps in 60d69b0.

// See LICENSE in the project root for license information.

import { JsonFile, JsonSchema, FileSystem } from '@rushstack/node-core-library';
import type { IExperimentsJson } from '@rushstack/rush-schemas/lib/experiments.zod';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - import this from the entrypoint of the package.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to import type { IExperimentsJson } from '@rushstack/rush-schemas' in 60d69b0. The API report now records the entrypoint instead of the subpath. While verifying this I also surfaced that rush-sdk's bundled dist/rush-lib.d.ts re-exports the type but didn't list @rushstack/rush-schemas as a dep — added it in 08e3937 so downstream consumers of rush-sdk resolve cleanly.

Copilot AI and others added 2 commits April 19, 2026 19:55
…oStateJson, package entrypoint imports

Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/83bb9e28-6062-4265-851a-ce82c95398ec

Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
…ntsJson re-export resolves

Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/83bb9e28-6062-4265-851a-ce82c95398ec

Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 19, 2026

@copilot - next steps:

  1. Make use of the other ported schemas in rush-lib...

Acknowledged — these four are clearly the right next steps but each is a meaningful, independently-validatable change, so I'd like to take them as a follow-up PR (or a small series) rather than fold them into this one. Quick sketch of how I'd approach each:

  1. Make use of the other ported schemas in rush-lib — wire cobuild first (smallest surface), then build-cache. For build-cache the JSON-Schema vs runtime-TS mismatch noted in the README means we either keep IBuildCacheJson hand-authored or rework it to drop the open [key: string]: JsonObject index signature; worth a focused discussion.
  2. Replace the .schema.json files in rush-lib's output with the ones from the schemas project — straightforward via a Heft copy-files-plugin task that copies node_modules/@rushstack/rush-schemas/lib-commonjs/*.schema.json into rush-lib's lib/schemas/. Need to delete the hand-authored copies in src/schemas/ in the same step and double-check no caller does JsonSchema.fromFile(__dirname + '/../schemas/...') outside the bundled set.
  3. Use zod to validate the loaded objects instead of JsonSchema — replace JsonFile.loadAndValidate call sites with experimentsSchema.parse(JsonFile.load(...)) (or a thin loadAndZodParse helper). Per-file migration; probably want to do it next to step 2 so we can drop the on-disk .schema.json artifact for the runtime path.
  4. Add zod schema support to node-core-library's JsonFile — most natural shape is an overload (or sibling helper) like JsonFile.loadAndZodParse(path, schema) returning z.infer<typeof schema>; can be implemented with a thin unknown-typed boundary so node-core-library doesn't take a real dependency on a specific zod major version.

Happy to open the first one of these when you're ready.

Copilot AI requested a review from iclanton April 19, 2026 20:00
Copy link
Copy Markdown
Member

@iclanton iclanton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - go ahead and do the work on those items. We can consider your work a first draft and tweak the design from there.

"phasesByName": {
"build": {
"tasksByName": {
"copy-json-schemas": {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - What is using this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the standard pattern every Heft plugin in this repo uses (e.g. heft-plugins/heft-lint-plugin/config/heft.json, heft-plugins/heft-typescript-plugin/config/heft.json, etc.) — the task hardlinks the plugin's optionsSchema (declared in heft-plugin.json) into temp/json-schemas/heft/v1/, and repo-toolbox collect-project-files --subfolder temp/json-schemas then aggregates those across all projects so the CI's "Collect JSON Schemas" step can publish them as a build artifact. See common/config/azure-pipelines/templates/post-publish.yaml and .github/workflows/ci.yml. Happy to remove it if we don't want this plugin's option schema published alongside the other Heft plugins, but it's there for parity with the rest of the Heft plugin family.

…as; add JsonFile.loadAndParse to node-core-library

Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/8313d468-a395-4e21-9308-46ee2c100ee0

Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 20, 2026

@copilot - go ahead and do the work on those items. We can consider your work a first draft and tweak the design from there.

First-draft of all four items pushed in 88fc2b6. Treating these as a starting point for design feedback rather than a finished form:

  1. Use the other ported schemas in rush-lib — wired cobuild and repo-state (in addition to experiments from the original PR). build-cache is intentionally still on the legacy path; the JSON-Schema-vs-runtime-TS mismatch noted in libraries/rush-schemas/README.md (provider-discriminated oneOf vs. open [key: string]: JsonObject extension shape) wants its own design discussion.
  2. Replace the .schema.json files in rush-lib's output — deleted experiments.schema.json / cobuild.schema.json / repo-state.schema.json from libraries/rush-lib/src/schemas/. They were only referenced via TS JSON imports inside the three configuration classes; with item 3 in play those imports go away too, so the files are simply removed (no need for a copy step from @rushstack/rush-schemas since rush-lib no longer consumes a .schema.json artifact for these three at runtime).
  3. Use zod to validate the loaded objects instead of JsonSchemaExperimentsConfiguration, CobuildConfiguration, and RepoStateFile now do JsonFile.loadAndParse(path, schemaFromRushSchemas) instead of JsonFile.loadAndValidate(path, jsonSchema). Bonus: ICobuildJson now re-exports from @rushstack/rush-schemas instead of being duplicated.
  4. Add zod schema support to node-core-library's JsonFile — added JsonFile.loadAndParse / loadAndParseAsync taking a structural IJsonFileTypeValidator<T> (any object with parse(input: unknown): T). This is the contract zod 3, zod 4, and most other validation libraries already match, so node-core-library doesn't take a runtime dep on a specific version.

Validated:

  • rush build --to @microsoft/rush-lib ✅, --to @rushstack/rush-sdk ✅, --to @rushstack/mcp-server ✅, --to @rushstack/node-core-library
  • rush-lib tests: 627/627 ✅
  • node-core-library tests: 267/267 ✅
  • API report regenerated; the only changes are ICobuildJson becoming a re-export and IExperimentsJson switching from a type-only to value+type re-export. Interface shapes are unchanged for downstream consumers.

Open design questions worth your eyes before treating this as more than a first draft:

  • Is the structural IJsonFileTypeValidator<T> the right shape, or would you rather take a real zod peer-dep in node-core-library and offer typed safeParse-style overloads (better error messages, but a hard dep)?
  • For experiments, experimentsSchema.parse() returns the inferred type (which structurally matches IExperimentsJson thanks to the rush-schemas drift assertion). I'm casting via as IExperimentsJson to preserve the hand-authored TSDoc'd public type. An alternative would be to return the inferred type directly and drop the hand-authored interface — at the cost of losing per-property TSDoc on the public API. Happy either way; deferring to your call.
  • Three rush-lib schemas migrated; the remaining ~15 are still on the legacy path. Pattern is now repeatable. Want me to follow with another batch, or would you rather review this first?

Copilot AI requested a review from iclanton April 20, 2026 02:42
*
* @beta
*/
export interface IExperimentsJson {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - Why are we defining this here instead of getting it from experimentsSchema?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair — switched in 429889d. IExperimentsJson is now Omit<z.infer<typeof experimentsSchema>, '$schema'> (same pattern as ICobuildJson), the drift-check assertion is gone, and the as IExperimentsJson cast in ExperimentsConfiguration.ts dropped out cleanly. Per-property docs now live on the schema's .describe() annotations as the single source of truth. Updated README.md and the now-obsolete cross-reference in cobuild.zod.ts to match.

Tradeoff worth flagging: TSDoc on individual experiment properties no longer renders in IDE tooltips for IExperimentsJson — only the schema's .describe() text is preserved (and that flows into the emitted experiments.schema.json). The interface-level TSDoc still shows. Tests: rush-lib 627/627 ✅.

…a via z.infer (drop hand-authored interface + drift check)

Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/df3800f0-fee3-4aeb-a16a-04079be1662c

Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Copilot AI requested a review from iclanton April 20, 2026 22:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Needs triage

Development

Successfully merging this pull request may close these issues.

2 participants