Skip to content

Add JSON result schema infrastructure - #8404

Open
gonzaloriestra wants to merge 1 commit into
gonzalo/json-side-events-infrastructurefrom
gonzalo/json-result-schema-infrastructure
Open

Add JSON result schema infrastructure#8404
gonzaloriestra wants to merge 1 commit into
gonzalo/json-side-events-infrastructurefrom
gonzalo/json-result-schema-infrastructure

Conversation

@gonzaloriestra

@gonzaloriestra gonzaloriestra commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

WHY are these changes introduced?

Closes shop/issues-develop#23660

Finite commands need one typed contract for validating JSON results and documenting them in command help.

Based on these prototypes:

WHAT is this pull request doing?

  • Adds typed JSON result schemas with validation and encoding.
  • Exposes schemas through BaseCommand and generated command help.
  • Rejects unsupported or unnamed nested schema constructs.

How to test your changes?

See #8415

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • I've considered analytics changes to measure impact
  • The change is user-facing — I've identified the correct bump type and added a changeset

@github-actions github-actions Bot added the Area: @shopify/cli @shopify/cli package issues label Aug 26, 2026
@gonzaloriestra
gonzaloriestra marked this pull request as ready for review August 27, 2026 11:42
@gonzaloriestra
gonzaloriestra requested review from a team as code owners August 27, 2026 11:42
@gonzaloriestra
gonzaloriestra marked this pull request as draft August 27, 2026 11:47
@gonzaloriestra
gonzaloriestra force-pushed the gonzalo/json-result-schema-infrastructure branch from 9541c57 to e8c7210 Compare August 27, 2026 11:55
@github-actions github-actions Bot added no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users. and removed Area: @shopify/cli @shopify/cli package issues labels Aug 27, 2026
@gonzaloriestra
gonzaloriestra force-pushed the gonzalo/json-result-schema-infrastructure branch from e8c7210 to e5133a5 Compare August 27, 2026 12:43
@github-actions github-actions Bot added Area: @shopify/cli @shopify/cli package issues and removed no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users. labels Aug 27, 2026
@gonzaloriestra
gonzaloriestra changed the base branch from main to gonzalo/json-side-events-infrastructure August 27, 2026 12:43
@gonzaloriestra
gonzaloriestra force-pushed the gonzalo/json-result-schema-infrastructure branch from e5133a5 to 91a047b Compare August 27, 2026 12:44
@github-actions github-actions Bot added no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users. and removed Area: @shopify/cli @shopify/cli package issues labels Aug 27, 2026
@gonzaloriestra
gonzaloriestra force-pushed the gonzalo/json-result-schema-infrastructure branch 3 times, most recently from fa0dbc1 to 0e3dfaf Compare August 27, 2026 14:56
@gonzaloriestra
gonzaloriestra force-pushed the gonzalo/json-result-schema-infrastructure branch from 0e3dfaf to 26b4b7c Compare August 28, 2026 12:07
@gonzaloriestra
gonzaloriestra force-pushed the gonzalo/json-result-schema-infrastructure branch from 26b4b7c to 04d0096 Compare August 28, 2026 12:29
@gonzaloriestra
gonzaloriestra marked this pull request as ready for review August 28, 2026 12:34
@gonzaloriestra
gonzaloriestra force-pushed the gonzalo/json-result-schema-infrastructure branch from 04d0096 to 02e9c32 Compare August 28, 2026 12:36

gonzaloriestra commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

@gonzaloriestra
gonzaloriestra force-pushed the gonzalo/json-result-schema-infrastructure branch from 6c746ea to 09fde41 Compare August 31, 2026 14:57
@github-actions

Copy link
Copy Markdown
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

packages/cli-kit/dist/public/common/command-events.d.ts
import { z } from 'zod';
/** Schema for a diagnostic emitted while a command executes. */
export declare const commandDiagnosticEventSchema: z.ZodObject<{
    type: z.ZodLiteral<"diagnostic">;
    timestamp: z.ZodString;
    level: z.ZodEnum<["debug", "info", "warning"]>;
    message: z.ZodString;
    code: z.ZodOptional<z.ZodString>;
}, "strict", z.ZodTypeAny, {
    type: "diagnostic";
    message: string;
    timestamp: string;
    level: "info" | "debug" | "warning";
    code?: string | undefined;
}, {
    type: "diagnostic";
    message: string;
    timestamp: string;
    level: "info" | "debug" | "warning";
    code?: string | undefined;
}>;
/** Schema for a progress update emitted while a command executes. */
export declare const commandProgressEventSchema: z.ZodObject<{
    type: z.ZodLiteral<"progress">;
    timestamp: z.ZodString;
    message: z.ZodString;
    current: z.ZodOptional<z.ZodNumber>;
    total: z.ZodOptional<z.ZodNumber>;
}, "strict", z.ZodTypeAny, {
    type: "progress";
    message: string;
    timestamp: string;
    current?: number | undefined;
    total?: number | undefined;
}, {
    type: "progress";
    message: string;
    timestamp: string;
    current?: number | undefined;
    total?: number | undefined;
}>;
/** Schema for side events emitted while a command executes. */
export declare const commandEventSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
    type: z.ZodLiteral<"diagnostic">;
    timestamp: z.ZodString;
    level: z.ZodEnum<["debug", "info", "warning"]>;
    message: z.ZodString;
    code: z.ZodOptional<z.ZodString>;
}, "strict", z.ZodTypeAny, {
    type: "diagnostic";
    message: string;
    timestamp: string;
    level: "info" | "debug" | "warning";
    code?: string | undefined;
}, {
    type: "diagnostic";
    message: string;
    timestamp: string;
    level: "info" | "debug" | "warning";
    code?: string | undefined;
}>, z.ZodObject<{
    type: z.ZodLiteral<"progress">;
    timestamp: z.ZodString;
    message: z.ZodString;
    current: z.ZodOptional<z.ZodNumber>;
    total: z.ZodOptional<z.ZodNumber>;
}, "strict", z.ZodTypeAny, {
    type: "progress";
    message: string;
    timestamp: string;
    current?: number | undefined;
    total?: number | undefined;
}, {
    type: "progress";
    message: string;
    timestamp: string;
    current?: number | undefined;
    total?: number | undefined;
}>]>;
/** A diagnostic emitted while a command executes. */
export type CommandDiagnosticEvent = z.infer<typeof commandDiagnosticEventSchema>;
/** A progress update emitted while a command executes. */
export type CommandProgressEvent = z.infer<typeof commandProgressEventSchema>;
/** A side event emitted while a command executes. */
export type CommandEvent = z.infer<typeof commandEventSchema>;
/** An event before its emission timestamp is added. */
export type CommandEventInput<TEvent extends CommandEvent = CommandEvent> = TEvent extends unknown ? Omit<TEvent, 'timestamp'> : never;
/** Presentation details that are not included in the emitted event. */
export interface CommandEventEmissionOptions {
    /** The event is already visible in the command's text UI. */
    alreadyRendered?: boolean;
}
/** Receives one timestamped event from a command execution. */
export type CommandEventSink<TEvent extends CommandEvent = CommandEvent> = (event: TEvent, options?: CommandEventEmissionOptions) => void;
/** Emits timestamped side events from one command execution. */
export interface CommandEventChannel<TEvent extends CommandEvent = CommandEvent> {
    emit: (event: CommandEventInput<TEvent>, options?: CommandEventEmissionOptions) => void;
}
/** Supplies the current time when an event is emitted. */
export type CommandEventClock = () => Date;
/** Options for a command event channel. */
export interface CommandEventChannelOptions<TEvent extends CommandEvent> {
    sink?: CommandEventSink<TEvent>;
    clock?: CommandEventClock;
}
/**
 * Creates a synchronous, execution-scoped channel for command side events.
 *
 * @param options - The event sink and clock used by the channel.
 * @returns A channel that adds an ISO timestamp before synchronously delivering each event.
 */
export declare function createCommandEventChannel<TEvent extends CommandEvent = CommandEvent>(options?: CommandEventChannelOptions<TEvent>): CommandEventChannel<TEvent>;
packages/cli-kit/dist/private/node/command-event-context.d.ts
import { type CommandEvent, type CommandEventChannelOptions, type CommandEventEmissionOptions, type CommandEventInput } from '../../public/common/command-events.js';
export type CommandEventOutputMode = 'text' | 'json';
interface RunWithCommandEventsOptions extends CommandEventChannelOptions<CommandEvent> {
    outputMode?: CommandEventOutputMode;
}
/**
 * Runs a command execution with an event channel available to all nested asynchronous work.
 *
 * @param options - The event sink, clock, and output mode used by the channel.
 * @param execute - The command execution to run with the channel.
 * @returns The result of the command execution.
 */
export declare function runWithCommandEvents<TResult>(options: RunWithCommandEventsOptions, execute: () => TResult): TResult;
/**
 * Emits an event for the current command execution.
 *
 * Events emitted outside a command execution are ignored.
 *
 * @param event - The event to emit before its timestamp is added.
 * @param options - Presentation details that are not included in the event.
 */
export declare function emitCommandEvent(event: CommandEventInput, options?: CommandEventEmissionOptions): void;
/**
 * Returns how command events are presented for the current execution.
 *
 * @returns The current event output mode, or undefined outside a command event context.
 */
export declare function commandEventOutputMode(): CommandEventOutputMode | undefined;
export {};
packages/cli-kit/dist/private/node/command-event-output.d.ts
import type { CommandEvent } from '../../public/common/command-events.js';
/**
 * Writes a command event as JSON without routing it back through the command event context.
 *
 * @param event - The event to write.
 */
export declare function outputCommandEventAsJson(event: CommandEvent): void;
packages/cli-kit/dist/public/node/command-events.d.ts
import { type CommandEvent, type CommandEventChannelOptions, type CommandEventEmissionOptions, type CommandEventInput } from '../common/command-events.js';
import { type CommandEventOutputMode } from '../../private/node/command-event-context.js';
export type { CommandEventOutputMode } from '../../private/node/command-event-context.js';
interface RunWithCommandEventsOptions extends CommandEventChannelOptions<CommandEvent> {
    outputMode?: CommandEventOutputMode;
}
/**
 * Runs a command execution with an event channel available to all nested asynchronous work.
 *
 * @param options - The event sink, clock, and output mode used by the channel.
 * @param execute - The command execution to run with the channel.
 * @returns The result of the command execution.
 */
export declare function runWithCommandEvents<TResult>(options: RunWithCommandEventsOptions, execute: () => TResult): TResult;
/**
 * Runs the complete CLI lifecycle with the event presentation selected by its arguments.
 *
 * @param argv - The command arguments used to determine whether JSON output is enabled.
 * @param execute - The command lifecycle to run.
 * @returns The result of the command lifecycle.
 */
export declare function runWithCommandEventsForCommand<TResult>(argv: string[], execute: () => TResult): TResult;
/**
 * Emits an event for the current command execution.
 *
 * Events emitted outside a command execution are ignored.
 *
 * @param event - The event to emit before its timestamp is added.
 * @param options - Presentation details that are not included in the event.
 */
export declare function emitCommandEvent(event: CommandEventInput, options?: CommandEventEmissionOptions): void;
/**
 * Returns how command events are presented for the current execution.
 *
 * @returns The current event output mode, or undefined outside a command event context.
 */
export declare function commandEventOutputMode(): CommandEventOutputMode | undefined;
/**
 * Renders a command side event to stderr using the existing CLI output behavior.
 *
 * @param event - The event to render.
 */
export declare function renderCommandEvent(event: CommandEvent): void;
/**
 * Renders a command side event as compact JSON to stderr.
 *
 * @param event - The event to render.
 */
export declare function renderCommandEventAsJson(event: CommandEvent): void;
packages/cli-kit/dist/public/node/json-output-schema.d.ts
import { ZodTypeAny, type z } from 'zod';
interface JsonOutputSchemaDefinition<TSchema extends ZodTypeAny = ZodTypeAny> {
    readonly name: string;
    readonly schema: TSchema;
    readonly definitions: Readonly<Record<string, ZodTypeAny>>;
}
export interface JsonOutputSchema<TSchema extends ZodTypeAny = ZodTypeAny> extends JsonOutputSchemaDefinition<TSchema> {
    readonly typescript: string;
    validate(value: unknown): z.output<TSchema>;
    encode(value: z.input<TSchema>): string;
}
export type InferJsonOutputSchema<TOutputSchema extends JsonOutputSchema> = z.output<TOutputSchema['schema']>;
interface DefineJsonOutputSchemaOptions<TSchema extends ZodTypeAny> {
    name: string;
    schema: TSchema;
    definitions?: Readonly<Record<string, ZodTypeAny>>;
}
/**
 * Defines the runtime validator, encoder, and documented TypeScript type for a command's JSON output.
 *
 * @param options - The root type name, its Zod schema, and any named nested schemas.
 * @returns The complete JSON output contract.
 */
export declare function defineJsonOutputSchema<TSchema extends ZodTypeAny>(options: DefineJsonOutputSchemaOptions<TSchema>): JsonOutputSchema<TSchema>;
/**
 * Renders the named schemas in a JSON output contract as TypeScript declarations.
 *
 * @param outputSchema - The root schema and its named nested schemas.
 * @returns TypeScript declarations suitable for command help.
 */
export declare function renderJsonOutputSchema(outputSchema: JsonOutputSchemaDefinition): string;
export {};

Existing type declarations

packages/cli-kit/dist/public/node/base-command.d.ts
@@ -1,5 +1,6 @@
 import { Command } from '@oclif/core';
 import { OutputFlags, Input, ParserOutput, FlagInput, OutputArgs } from '@oclif/core/parser';
+import type { JsonOutputSchema } from './json-output-schema.js';
 export type ArgOutput = OutputArgs<any>;
 export type FlagOutput = OutputFlags<any>;
 export interface NonTTYFlagRequirement {
@@ -10,6 +11,8 @@ export interface NonTTYFlagRequirement {
 }
 declare abstract class BaseCommand extends Command {
     static baseFlags: FlagInput<{}>;
+    static descriptionWithMarkdown?: string;
+    static get jsonOutputSchema(): JsonOutputSchema | undefined;
     static get requiresSyncAnalytics(): boolean;
     static nonTTYFlagRequirements(_flags: FlagOutput): NonTTYFlagRequirement[];
     static descriptionWithoutMarkdown(): string | undefined;
@@ -18,6 +21,7 @@ declare abstract class BaseCommand extends Command {
     catch(error: Error & {
         skipOclifErrorHandling: boolean;
     }): Promise<void>;
+    protected _run<T>(): Promise<T>;
     protected init(): Promise<unknown>;
     protected showNpmFlagWarning(): void;
     protected exitWithTimestampWhenEnvVariablePresent(): void;
packages/cli-kit/dist/public/node/environment.d.ts
@@ -42,9 +42,10 @@ export declare function getIdentityTokenInformation(): {
  * Checks if the JSON output is enabled via flag (--json or -j) or environment variable (SHOPIFY_FLAG_JSON).
  *
  * @param environment - Process environment variables.
+ * @param argv - Command arguments to inspect for JSON flags.
  * @returns True if the JSON output is enabled, false otherwise.
  */
-export declare function jsonOutputEnabled(environment?: NodeJS.ProcessEnv): boolean;
+export declare function jsonOutputEnabled(environment?: NodeJS.ProcessEnv, argv?: string[]): boolean;
 /**
  * If true, the CLI should not use the network level retry.
  *

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant