Skip to content

Commit 88fc2b6

Browse files
Copiloticlanton
andauthored
feat(rush-lib): validate experiments/cobuild/repo-state via zod schemas; 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>
1 parent 08e3937 commit 88fc2b6

10 files changed

Lines changed: 79 additions & 193 deletions

File tree

common/reviews/api/node-core-library.api.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,11 @@ export interface IJsonFileStringifyOptions extends IJsonFileParseOptions {
457457
prettyFormatting?: boolean;
458458
}
459459

460+
// @public
461+
export interface IJsonFileTypeValidator<T> {
462+
parse(input: unknown): T;
463+
}
464+
460465
// @public
461466
export interface IJsonSchemaCustomFormat<T extends string | number> {
462467
type: T extends string ? 'string' : T extends number ? 'number' : never;
@@ -732,6 +737,8 @@ export class JsonFile {
732737
// @internal (undocumented)
733738
static _formatPathForError: (path: string) => string;
734739
static load(jsonFilename: string, options?: IJsonFileParseOptions): JsonObject;
740+
static loadAndParse<T>(jsonFilename: string, validator: IJsonFileTypeValidator<T>, options?: IJsonFileParseOptions): T;
741+
static loadAndParseAsync<T>(jsonFilename: string, validator: IJsonFileTypeValidator<T>, options?: IJsonFileParseOptions): Promise<T>;
735742
static loadAndValidate(jsonFilename: string, jsonSchema: JsonSchema, options?: IJsonFileLoadAndValidateOptions): JsonObject;
736743
static loadAndValidateAsync(jsonFilename: string, jsonSchema: JsonSchema, options?: IJsonFileLoadAndValidateOptions): Promise<JsonObject>;
737744
static loadAndValidateWithCallback(jsonFilename: string, jsonSchema: JsonSchema, errorCallback: (errorInfo: IJsonSchemaErrorInfo) => void, options?: IJsonFileLoadAndValidateOptions): JsonObject;

common/reviews/api/rush-lib.api.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import type { CommandLineParameter } from '@rushstack/ts-command-line';
1515
import { CommandLineParameterKind } from '@rushstack/ts-command-line';
1616
import { CredentialCache } from '@rushstack/credential-cache';
1717
import { HookMap } from 'tapable';
18+
import { ICobuildJson } from '@rushstack/rush-schemas';
1819
import { ICredentialCacheEntry } from '@rushstack/credential-cache';
1920
import { ICredentialCacheOptions } from '@rushstack/credential-cache';
20-
import type { IExperimentsJson } from '@rushstack/rush-schemas';
21+
import { IExperimentsJson } from '@rushstack/rush-schemas';
2122
import { IFileDiffStatus } from '@rushstack/package-deps-hash';
2223
import { IPackageJson } from '@rushstack/node-core-library';
2324
import { IPrefixMatch } from '@rushstack/lookup-by-path';
@@ -376,13 +377,7 @@ export interface ICobuildContext {
376377
runnerId: string;
377378
}
378379

379-
// @beta (undocumented)
380-
export interface ICobuildJson {
381-
// (undocumented)
382-
cobuildFeatureEnabled: boolean;
383-
// (undocumented)
384-
cobuildLockProvider: string;
385-
}
380+
export { ICobuildJson }
386381

387382
// @beta (undocumented)
388383
export interface ICobuildLockProvider {

libraries/node-core-library/src/JsonFile.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,25 @@ export interface IJsonFileParseOptions {
124124
*/
125125
export interface IJsonFileLoadAndValidateOptions extends IJsonFileParseOptions, IJsonSchemaValidateOptions {}
126126

127+
/**
128+
* A structural validator interface that matches the parse/safeParse contract of
129+
* popular schema libraries such as zod.
130+
*
131+
* @remarks
132+
* `JsonFile.loadAndParse()` accepts any object whose `parse()` method takes an
133+
* `unknown` input and returns a typed value (throwing on validation failure).
134+
* Using a structural type here avoids forcing `node-core-library` to take a
135+
* runtime dependency on a specific schema-validation library or major version.
136+
*
137+
* @public
138+
*/
139+
export interface IJsonFileTypeValidator<T> {
140+
/**
141+
* Validate `input` and return it as the validated type, or throw if validation fails.
142+
*/
143+
parse(input: unknown): T;
144+
}
145+
127146
/**
128147
* Options for {@link JsonFile.stringify}
129148
*
@@ -319,6 +338,45 @@ export class JsonFile {
319338
return jsonObject;
320339
}
321340

341+
/**
342+
* Loads a JSON file and validates it using a structural validator (such as a
343+
* zod schema), returning the strongly-typed result.
344+
*
345+
* @remarks
346+
* `validator` is any object exposing a `parse(input: unknown): T` method.
347+
* The validator is responsible for both runtime validation and the resulting
348+
* TypeScript type. This indirection lets `node-core-library` accept zod
349+
* schemas without taking a runtime dependency on zod.
350+
*
351+
* @example
352+
* ```ts
353+
* import { z } from 'zod';
354+
* const schema = z.object({ name: z.string() });
355+
* const data = JsonFile.loadAndParse('config.json', schema);
356+
* // data is typed as { name: string }
357+
* ```
358+
*/
359+
public static loadAndParse<T>(
360+
jsonFilename: string,
361+
validator: IJsonFileTypeValidator<T>,
362+
options?: IJsonFileParseOptions
363+
): T {
364+
const jsonObject: JsonObject = JsonFile.load(jsonFilename, options);
365+
return validator.parse(jsonObject);
366+
}
367+
368+
/**
369+
* An async version of {@link JsonFile.loadAndParse}.
370+
*/
371+
public static async loadAndParseAsync<T>(
372+
jsonFilename: string,
373+
validator: IJsonFileTypeValidator<T>,
374+
options?: IJsonFileParseOptions
375+
): Promise<T> {
376+
const jsonObject: JsonObject = await JsonFile.loadAsync(jsonFilename, options);
377+
return validator.parse(jsonObject);
378+
}
379+
322380
/**
323381
* Serializes the specified JSON object to a string buffer.
324382
* @param jsonObject - the object to be serialized

libraries/node-core-library/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export {
105105
type IJsonFileLoadAndValidateOptions,
106106
type IJsonFileStringifyOptions,
107107
type IJsonFileSaveOptions,
108+
type IJsonFileTypeValidator,
108109
JsonFile
109110
} from './JsonFile';
110111

libraries/rush-lib/src/api/CobuildConfiguration.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,17 @@
33

44
import { randomUUID } from 'node:crypto';
55

6-
import { FileSystem, JsonFile, JsonSchema } from '@rushstack/node-core-library';
6+
import { FileSystem, JsonFile } from '@rushstack/node-core-library';
7+
import { type ICobuildJson, cobuildSchema } from '@rushstack/rush-schemas';
78
import type { ITerminal } from '@rushstack/terminal';
89

910
import { EnvironmentConfiguration } from './EnvironmentConfiguration';
1011
import type { CobuildLockProviderFactory, RushSession } from '../pluginFramework/RushSession';
1112
import { RushConstants } from '../logic/RushConstants';
1213
import type { ICobuildLockProvider } from '../logic/cobuild/ICobuildLockProvider';
1314
import type { RushConfiguration } from './RushConfiguration';
14-
import schemaJson from '../schemas/cobuild.schema.json';
1515

16-
/**
17-
* @beta
18-
*/
19-
export interface ICobuildJson {
20-
cobuildFeatureEnabled: boolean;
21-
cobuildLockProvider: string;
22-
}
16+
export type { ICobuildJson };
2317

2418
/**
2519
* @beta
@@ -37,8 +31,6 @@ export interface ICobuildConfigurationOptions {
3731
* @beta
3832
*/
3933
export class CobuildConfiguration {
40-
private static _jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(schemaJson);
41-
4234
/**
4335
* Indicates whether the cobuild feature is enabled.
4436
* Typically it is enabled in the cobuild.json config file.
@@ -126,7 +118,7 @@ export class CobuildConfiguration {
126118
): Promise<CobuildConfiguration | undefined> {
127119
let cobuildJson: ICobuildJson | undefined;
128120
try {
129-
cobuildJson = await JsonFile.loadAndValidateAsync(jsonFilePath, CobuildConfiguration._jsonSchema);
121+
cobuildJson = await JsonFile.loadAndParseAsync(jsonFilePath, cobuildSchema);
130122
} catch (e) {
131123
if (FileSystem.isNotExistError(e)) {
132124
return undefined;

libraries/rush-lib/src/api/ExperimentsConfiguration.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4-
import { JsonFile, JsonSchema, FileSystem } from '@rushstack/node-core-library';
5-
import type { IExperimentsJson } from '@rushstack/rush-schemas';
4+
import { JsonFile, FileSystem } from '@rushstack/node-core-library';
5+
import { type IExperimentsJson, experimentsSchema } from '@rushstack/rush-schemas';
66
import { Colorize } from '@rushstack/terminal';
77

8-
import schemaJson from '../schemas/experiments.schema.json';
9-
108
export type { IExperimentsJson };
119

1210
const GRADUATED_EXPERIMENTS: Set<string> = new Set(['phasedCommands']);
1311

14-
15-
const _EXPERIMENTS_JSON_SCHEMA: JsonSchema = JsonSchema.fromLoadedObject(schemaJson);
16-
1712
/**
1813
* Use this class to load the "common/config/rush/experiments.json" config file.
1914
* This file allows repo maintainers to enable and disable experimental Rush features.
@@ -31,7 +26,7 @@ export class ExperimentsConfiguration {
3126
*/
3227
public constructor(jsonFilePath: string) {
3328
try {
34-
this.configuration = JsonFile.loadAndValidate(jsonFilePath, _EXPERIMENTS_JSON_SCHEMA);
29+
this.configuration = JsonFile.loadAndParse(jsonFilePath, experimentsSchema) as IExperimentsJson;
3530
} catch (e) {
3631
if (FileSystem.isNotExistError(e)) {
3732
this.configuration = {};

libraries/rush-lib/src/logic/RepoStateFile.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4-
import { FileSystem, JsonFile, JsonSchema, NewlineKind } from '@rushstack/node-core-library';
4+
import { FileSystem, JsonFile, NewlineKind } from '@rushstack/node-core-library';
5+
import { repoStateSchema } from '@rushstack/rush-schemas';
56

67
import type { RushConfiguration } from '../api/RushConfiguration';
78
import { PnpmShrinkwrapFile } from './pnpm/PnpmShrinkwrapFile';
89
import type { CommonVersionsConfiguration } from '../api/CommonVersionsConfiguration';
9-
import schemaJson from '../schemas/repo-state.schema.json';
1010
import type { Subspace } from '../api/Subspace';
1111

1212
/**
@@ -45,8 +45,6 @@ interface IRepoStateJson {
4545
* @public
4646
*/
4747
export class RepoStateFile {
48-
private static _jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(schemaJson);
49-
5048
private _pnpmShrinkwrapHash: string | undefined;
5149
private _preferredVersionsHash: string | undefined;
5250
private _packageJsonInjectedDependenciesHash: string | undefined;
@@ -148,7 +146,7 @@ export class RepoStateFile {
148146
}
149147

150148
if (repoStateJson) {
151-
this._jsonSchema.validateObject(repoStateJson, jsonFilename);
149+
repoStateSchema.parse(repoStateJson);
152150
}
153151
}
154152

libraries/rush-lib/src/schemas/cobuild.schema.json

Lines changed: 0 additions & 35 deletions
This file was deleted.

libraries/rush-lib/src/schemas/experiments.schema.json

Lines changed: 0 additions & 95 deletions
This file was deleted.

libraries/rush-lib/src/schemas/repo-state.schema.json

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)