Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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\"",
Expand Down
10 changes: 8 additions & 2 deletions packages/store/src/cli/commands/store/create/preview.test.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -23,6 +24,7 @@ describe('store create preview command', () => {
country: 'US',
storefrontUrl: 'https://x.myshopify.com',
},
next_steps: [],
}
vi.mocked(createPreviewStoreCommand).mockResolvedValueOnce(result)

Expand All @@ -33,12 +35,16 @@ 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 () => {
await expect(StoreCreatePreview.run(['--country', 'USA'])).rejects.toThrow('process.exit unexpectedly called')

expect(createPreviewStoreCommand).not.toHaveBeenCalled()
})

test('exposes the JSON output schema', () => {
expect(StoreCreatePreview.jsonOutputSchema).toBe(createPreviewStoreJsonOutputSchema)
})
})
14 changes: 11 additions & 3 deletions packages/store/src/cli/commands/store/create/preview.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -31,6 +35,10 @@ export default class StoreCreatePreview extends StoreCommand {
country: countryFlag,
}

static get jsonOutputSchema() {
return createPreviewStoreJsonOutputSchema
}

public async run(): Promise<void> {
const {flags} = await this.parse(StoreCreatePreview)

Expand All @@ -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')
}
}
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 () => {
Expand Down
22 changes: 10 additions & 12 deletions packages/store/src/cli/services/store/create/preview/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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.`,
]
}
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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(
Expand Down Expand Up @@ -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({
Expand Down
50 changes: 8 additions & 42 deletions packages/store/src/cli/services/store/create/preview/result.ts
Original file line number Diff line number Diff line change
@@ -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<InlineToken>
}

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<InlineToken> {
return nextStep
.split(/(`[^`]+`)/)
.map((part) => (part.startsWith('`') && part.endsWith('`') ? {command: part.slice(1, -1)} : part))
}

function renderTextResult(result: CreatePreviewStoreResult): void {
Expand All @@ -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),
},
},
},
Expand Down
23 changes: 23 additions & 0 deletions packages/store/src/cli/services/store/create/preview/types.ts
Original file line number Diff line number Diff line change
@@ -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<typeof createPreviewStoreJsonOutputSchema>
Loading