From 8e08e29831d9e83718062bd87a8a0bd0ce35a03f Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Mon, 31 Aug 2026 13:18:18 +0200 Subject: [PATCH] Add typed JSON output to store creation --- packages/cli/README.md | 21 ++++++++ packages/cli/oclif.manifest.json | 2 +- .../cli/commands/store/create/preview.test.ts | 10 +++- .../src/cli/commands/store/create/preview.ts | 14 ++++-- .../store/create/preview/index.test.ts | 7 +++ .../services/store/create/preview/index.ts | 22 ++++---- .../store/create/preview/result.test.ts | 11 ++-- .../services/store/create/preview/result.ts | 50 +++---------------- .../services/store/create/preview/types.ts | 23 +++++++++ 9 files changed, 97 insertions(+), 63 deletions(-) create mode 100644 packages/store/src/cli/services/store/create/preview/types.ts diff --git a/packages/cli/README.md b/packages/cli/README.md index 1d2b03449ad..72e3701084c 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -4000,6 +4000,27 @@ DESCRIPTION Creates a new Shopify store, with no need for an existing account. + Output from `--json` conforms to the `CreatePreviewStoreResult` schema. + + Use `--json-schema` to print the schema directly: + + ```ts + interface CreatePreviewStoreResult { + status: "success" + message: string + store: PreviewStore + next_steps: string[] + } + + interface PreviewStore { + id: string + name: string + subdomain: string + country?: string + storefrontUrl: string + } + ``` + EXAMPLES $ shopify store create preview --name "Lavender Candles" diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 66513bcce84..261e43360d8 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -7421,7 +7421,7 @@ "args": { }, "customPluginName": "@shopify/store", - "description": "Creates a new Shopify store, with no need for an existing account.", + "description": "Creates a new Shopify store, with no need for an existing account.\n\nOutput from `--json` conforms to the `CreatePreviewStoreResult` schema.\n\nUse `--json-schema` to print the schema directly:\n\n```ts\ninterface CreatePreviewStoreResult {\n status: \"success\"\n message: string\n store: PreviewStore\n next_steps: string[]\n}\n\ninterface PreviewStore {\n id: string\n name: string\n subdomain: string\n country?: string\n storefrontUrl: string\n}\n```", "descriptionWithMarkdown": "Creates a new Shopify store, with no need for an existing account.", "examples": [ "<%= config.bin %> <%= command.id %> --name \"Lavender Candles\"", diff --git a/packages/store/src/cli/commands/store/create/preview.test.ts b/packages/store/src/cli/commands/store/create/preview.test.ts index 9781d98bc63..d1f94d0642f 100644 --- a/packages/store/src/cli/commands/store/create/preview.test.ts +++ b/packages/store/src/cli/commands/store/create/preview.test.ts @@ -1,6 +1,7 @@ import StoreCreatePreview from './preview.js' import {createPreviewStoreCommand} from '../../../services/store/create/preview/index.js' -import {writeCreatePreviewStoreResult} from '../../../services/store/create/preview/result.js' +import {presentCreatePreviewStoreResult} from '../../../services/store/create/preview/result.js' +import {createPreviewStoreJsonOutputSchema} from '../../../services/store/create/preview/types.js' import {renderSingleTask} from '@shopify/cli-kit/node/ui' import {describe, expect, test, vi} from 'vitest' @@ -23,6 +24,7 @@ describe('store create preview command', () => { country: 'US', storefrontUrl: 'https://x.myshopify.com', }, + next_steps: [], } vi.mocked(createPreviewStoreCommand).mockResolvedValueOnce(result) @@ -33,7 +35,7 @@ describe('store create preview command', () => { task: expect.any(Function), }) expect(createPreviewStoreCommand).toHaveBeenCalledWith({name: 'Lavender Candles', country: 'US'}) - expect(writeCreatePreviewStoreResult).toHaveBeenCalledWith(result, 'json') + expect(presentCreatePreviewStoreResult).toHaveBeenCalledWith(result, 'json') }) test('rejects invalid country codes before calling the service', async () => { @@ -41,4 +43,8 @@ describe('store create preview command', () => { expect(createPreviewStoreCommand).not.toHaveBeenCalled() }) + + test('exposes the JSON output schema', () => { + expect(StoreCreatePreview.jsonOutputSchema).toBe(createPreviewStoreJsonOutputSchema) + }) }) diff --git a/packages/store/src/cli/commands/store/create/preview.ts b/packages/store/src/cli/commands/store/create/preview.ts index f5dc5564f7a..5f7a25e0061 100644 --- a/packages/store/src/cli/commands/store/create/preview.ts +++ b/packages/store/src/cli/commands/store/create/preview.ts @@ -1,6 +1,10 @@ import {countryFlag} from '../../../flags.js' -import {type CreatePreviewStoreResult, createPreviewStoreCommand} from '../../../services/store/create/preview/index.js' -import {writeCreatePreviewStoreResult} from '../../../services/store/create/preview/result.js' +import {createPreviewStoreCommand} from '../../../services/store/create/preview/index.js' +import {presentCreatePreviewStoreResult} from '../../../services/store/create/preview/result.js' +import { + createPreviewStoreJsonOutputSchema, + type CreatePreviewStoreResult, +} from '../../../services/store/create/preview/types.js' import StoreCommand from '../../../utilities/store-command.js' import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' import {outputContent} from '@shopify/cli-kit/node/output' @@ -31,6 +35,10 @@ export default class StoreCreatePreview extends StoreCommand { country: countryFlag, } + static get jsonOutputSchema() { + return createPreviewStoreJsonOutputSchema + } + public async run(): Promise { const {flags} = await this.parse(StoreCreatePreview) @@ -39,6 +47,6 @@ export default class StoreCreatePreview extends StoreCommand { task: async () => createPreviewStoreCommand({name: flags.name, country: flags.country}), }) - writeCreatePreviewStoreResult(result, flags.json ? 'json' : 'text') + presentCreatePreviewStoreResult(result, flags.json ? 'json' : 'text') } } diff --git a/packages/store/src/cli/services/store/create/preview/index.test.ts b/packages/store/src/cli/services/store/create/preview/index.test.ts index a1e46275219..4261b78974a 100644 --- a/packages/store/src/cli/services/store/create/preview/index.test.ts +++ b/packages/store/src/cli/services/store/create/preview/index.test.ts @@ -1,4 +1,5 @@ import {createPreviewStoreCommand} from './index.js' +import {createPreviewStoreJsonOutputSchema} from './types.js' import {STORE_AUTH_APP_CLIENT_ID} from '../../auth/config.js' import {describe, expect, test, vi} from 'vitest' import type {PreviewStoreClientOptions} from './client.js' @@ -57,7 +58,13 @@ describe('preview store create service', () => { country: 'US', storefrontUrl: 'https://app.shopify.com/auth/preview-store?token=access-token', }, + next_steps: [ + 'Use `shopify store open --store x12y45z.myshopify.com` to preview the storefront.', + 'Use `shopify store execute --store x12y45z.myshopify.com` to add products, collections, pages, and more.', + 'Use `shopify theme pull --store x12y45z.myshopify.com` and `shopify theme push --store x12y45z.myshopify.com` to edit your store design.', + ], }) + expect(createPreviewStoreJsonOutputSchema.validate(result)).toEqual(result) }) test('uses the shop id as the preview user id when no placeholder account uuid is returned', async () => { diff --git a/packages/store/src/cli/services/store/create/preview/index.ts b/packages/store/src/cli/services/store/create/preview/index.ts index 4fd2d111156..a27e1d8529f 100644 --- a/packages/store/src/cli/services/store/create/preview/index.ts +++ b/packages/store/src/cli/services/store/create/preview/index.ts @@ -1,4 +1,5 @@ import {PreviewStoreClientOptions, PreviewStoreCreateResponse, createPreviewStore} from './client.js' +import {type CreatePreviewStoreResult} from './types.js' import {STORE_AUTH_APP_CLIENT_ID} from '../../auth/config.js' import {recordStoreFqdnMetadata} from '../../attribution.js' import {setStoredStoreAppSession} from '@shopify/cli-kit/node/store-auth-session' @@ -18,18 +19,6 @@ interface CreatePreviewStoreDependencies { now: () => Date } -export interface CreatePreviewStoreResult { - status: 'success' - message: string - store: { - id: string - name: string - subdomain: string - country?: string - storefrontUrl: string - } -} - const defaultDependencies: CreatePreviewStoreDependencies = { createPreviewStore, setStoredStoreAppSession, @@ -101,5 +90,14 @@ async function persistPreviewStoreSession( ...(country ? {country} : {}), storefrontUrl: response.accessUrl, }, + next_steps: previewStoreNextSteps(response.shop.domain), } } + +function previewStoreNextSteps(store: string): string[] { + return [ + `Use \`shopify store open --store ${store}\` to preview the storefront.`, + `Use \`shopify store execute --store ${store}\` to add products, collections, pages, and more.`, + `Use \`shopify theme pull --store ${store}\` and \`shopify theme push --store ${store}\` to edit your store design.`, + ] +} diff --git a/packages/store/src/cli/services/store/create/preview/result.test.ts b/packages/store/src/cli/services/store/create/preview/result.test.ts index 5e540f2b63f..32f32a55c52 100644 --- a/packages/store/src/cli/services/store/create/preview/result.test.ts +++ b/packages/store/src/cli/services/store/create/preview/result.test.ts @@ -1,4 +1,4 @@ -import {writeCreatePreviewStoreResult} from './result.js' +import {presentCreatePreviewStoreResult} from './result.js' import {outputResult} from '@shopify/cli-kit/node/output' import {renderSuccess} from '@shopify/cli-kit/node/ui' import {describe, expect, test, vi} from 'vitest' @@ -23,11 +23,16 @@ const result = { country: 'US', storefrontUrl: 'https://x12y45z.myshopify.com/?foo=bar', }, + next_steps: [ + 'Use `shopify store open --store x12y45z.myshopify.com` to preview the storefront.', + 'Use `shopify store execute --store x12y45z.myshopify.com` to add products, collections, pages, and more.', + 'Use `shopify theme pull --store x12y45z.myshopify.com` and `shopify theme push --store x12y45z.myshopify.com` to edit your store design.', + ], } describe('preview store create result presenter', () => { test('writes JSON output with the next steps', () => { - writeCreatePreviewStoreResult(result, 'json') + presentCreatePreviewStoreResult(result, 'json') expect(outputResult).toHaveBeenCalledWith( JSON.stringify( @@ -55,7 +60,7 @@ describe('preview store create result presenter', () => { }) test('renders text output with store details and next steps', () => { - writeCreatePreviewStoreResult(result, 'text') + presentCreatePreviewStoreResult(result, 'text') expect(renderSuccess).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/packages/store/src/cli/services/store/create/preview/result.ts b/packages/store/src/cli/services/store/create/preview/result.ts index bf167aa2e48..7c3c2519f62 100644 --- a/packages/store/src/cli/services/store/create/preview/result.ts +++ b/packages/store/src/cli/services/store/create/preview/result.ts @@ -1,59 +1,25 @@ -import {type CreatePreviewStoreResult} from './index.js' +import {createPreviewStoreJsonOutputSchema, type CreatePreviewStoreResult} from './types.js' import {outputResult} from '@shopify/cli-kit/node/output' import {renderSuccess, type InlineToken, type TokenItem} from '@shopify/cli-kit/node/ui' type CreatePreviewStoreOutputFormat = 'text' | 'json' -interface PreviewStoreNextStep { - json: string - text: TokenItem -} -export function writeCreatePreviewStoreResult( +export function presentCreatePreviewStoreResult( result: CreatePreviewStoreResult, format: CreatePreviewStoreOutputFormat, ): void { if (format === 'json') { - outputResult(JSON.stringify(serializeAsJson(result), null, 2)) + outputResult(createPreviewStoreJsonOutputSchema.encode(result)) return } renderTextResult(result) } -function serializeAsJson(result: CreatePreviewStoreResult) { - return { - status: result.status, - message: result.message, - store: result.store, - next_steps: previewStoreNextSteps(result).map((step) => step.json), - } -} - -function previewStoreNextSteps(result: CreatePreviewStoreResult): PreviewStoreNextStep[] { - return [ - { - json: `Use \`shopify store open --store ${result.store.subdomain}\` to preview the storefront.`, - text: ['Use ', {command: `shopify store open --store ${result.store.subdomain}`}, ' to preview the storefront.'], - }, - { - json: `Use \`shopify store execute --store ${result.store.subdomain}\` to add products, collections, pages, and more.`, - text: [ - 'Use ', - {command: `shopify store execute --store ${result.store.subdomain}`}, - ' to add products, collections, pages, and more.', - ], - }, - { - json: `Use \`shopify theme pull --store ${result.store.subdomain}\` and \`shopify theme push --store ${result.store.subdomain}\` to edit your store design.`, - text: [ - 'Use ', - {command: `shopify theme pull --store ${result.store.subdomain}`}, - ' and ', - {command: `shopify theme push --store ${result.store.subdomain}`}, - ' to edit your store design.', - ], - }, - ] +function tokenizeNextStep(nextStep: string): TokenItem { + return nextStep + .split(/(`[^`]+`)/) + .map((part) => (part.startsWith('`') && part.endsWith('`') ? {command: part.slice(1, -1)} : part)) } function renderTextResult(result: CreatePreviewStoreResult): void { @@ -69,7 +35,7 @@ function renderTextResult(result: CreatePreviewStoreResult): void { title: 'Next steps', body: { list: { - items: previewStoreNextSteps(result).map((step) => step.text), + items: result.next_steps.map(tokenizeNextStep), }, }, }, diff --git a/packages/store/src/cli/services/store/create/preview/types.ts b/packages/store/src/cli/services/store/create/preview/types.ts new file mode 100644 index 00000000000..f7f3561bae1 --- /dev/null +++ b/packages/store/src/cli/services/store/create/preview/types.ts @@ -0,0 +1,23 @@ +import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {zod} from '@shopify/cli-kit/node/schema' + +const PreviewStoreSchema = zod.object({ + id: zod.string(), + name: zod.string(), + subdomain: zod.string(), + country: zod.string().optional(), + storefrontUrl: zod.string(), +}) + +export const createPreviewStoreJsonOutputSchema = defineJsonOutputSchema({ + name: 'CreatePreviewStoreResult', + schema: zod.object({ + status: zod.literal('success'), + message: zod.string(), + store: PreviewStoreSchema, + next_steps: zod.array(zod.string()), + }), + definitions: {PreviewStore: PreviewStoreSchema}, +}) + +export type CreatePreviewStoreResult = InferJsonOutputSchema