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
65 changes: 65 additions & 0 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7979,6 +7979,71 @@
"strict": true,
"summary": "Renames an existing theme."
},
"theme:sandbox-preview": {
"aliases": [
],
"args": {
},
"customPluginName": "@shopify/theme",
"description": "Starts an authless, one-shot sandbox preview using \"mock.shop\" (https://mock.shop/).\n\nThis prototype writes a local launcher page and opens it in your browser. The launcher then POSTs the override payload directly to the target storefront to render an initial preview.\n\n- No store authentication is required\n- No preview is persisted\n- Navigation after the first page load will not preserve overrides",
"descriptionWithMarkdown": "Starts an authless, one-shot sandbox preview using [mock.shop](https://mock.shop/).\n\nThis prototype writes a local launcher page and opens it in your browser. The launcher then POSTs the override payload directly to the target storefront to render an initial preview.\n\n- No store authentication is required\n- No preview is persisted\n- Navigation after the first page load will not preserve overrides",
"enableJsonFlag": false,
"flags": {
"no-color": {
"allowNo": false,
"description": "Disable color output.",
"env": "SHOPIFY_FLAG_NO_COLOR",
"hidden": false,
"name": "no-color",
"type": "boolean"
},
"no-open": {
"allowNo": false,
"description": "Do not automatically launch the local storefront preview launcher in your default web browser.",
"env": "SHOPIFY_FLAG_NO_OPEN",
"name": "no-open",
"type": "boolean"
},
"overrides": {
"description": "Path to a JSON overrides file.",
"env": "SHOPIFY_FLAG_OVERRIDES",
"hasDynamicHelp": false,
"multiple": false,
"name": "overrides",
"required": true,
"type": "option"
},
"storefront-url": {
"description": "Override the storefront preview target. Useful for local SFR testing.",
"env": "SHOPIFY_FLAG_STOREFRONT_URL",
"hasDynamicHelp": false,
"multiple": false,
"name": "storefront-url",
"required": false,
"type": "option"
},
"verbose": {
"allowNo": false,
"description": "Increase the verbosity of the output.",
"env": "SHOPIFY_FLAG_VERBOSE",
"hidden": false,
"name": "verbose",
"type": "boolean"
}
},
"hasDynamicHelp": false,
"hiddenAliases": [
],
"id": "theme:sandbox-preview",
"pluginAlias": "@shopify/cli",
"pluginName": "@shopify/cli",
"pluginType": "core",
"strict": true,
"summary": "Opens a one-shot authless mock.shop sandbox preview from a JSON overrides file.",
"usage": [
"theme sandbox-preview --overrides path/to/overrides.json"
]
},
"theme:serve": {
"aliases": [
],
Expand Down
50 changes: 50 additions & 0 deletions packages/theme/src/cli/commands/theme/preview/mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {devWithOverrideFile} from '../../../services/dev-override.js'
import {Flags} from '@oclif/core'
import Command from '@shopify/cli-kit/node/base-command'
import {globalFlags} from '@shopify/cli-kit/node/cli'

export default class PreviewMock extends Command {
static summary = 'Opens a one-shot authless mock.shop sandbox preview from a JSON overrides file.'

static usage = ['theme preview mock --overrides path/to/overrides.json']

static descriptionWithMarkdown = `Starts an authless, one-shot sandbox preview using [mock.shop](https://mock.shop/).

This prototype writes a local launcher page and opens it in your browser. The launcher then POSTs the override payload directly to the target storefront to render an initial preview.

- No store authentication is required
- No preview is persisted
- Navigation after the first page load will not preserve overrides`

static description = this.descriptionWithoutMarkdown()

static flags = {
...globalFlags,
overrides: Flags.string({
description: 'Path to a JSON overrides file.',
env: 'SHOPIFY_FLAG_OVERRIDES',
required: true,
}),
open: Flags.boolean({
description: 'Automatically launch the local storefront preview launcher in your default web browser.',
env: 'SHOPIFY_FLAG_OPEN',
default: true,
}),
'storefront-url': Flags.string({
description: 'Override the storefront preview target. Useful for local SFR testing.',
env: 'SHOPIFY_FLAG_STOREFRONT_URL',
required: false,
}),
}

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

await devWithOverrideFile({
overrideJson: flags.overrides,
open: flags.open,
mockShop: true,
mockShopStorefrontUrl: flags['storefront-url'],
})
}
}
53 changes: 53 additions & 0 deletions packages/theme/src/cli/commands/theme/sandbox-preview.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import SandboxPreview from './sandbox-preview.js'
import {devWithOverrideFile} from '../../services/dev-override.js'
import {Config} from '@oclif/core'
import {describe, vi, expect, test, beforeEach} from 'vitest'

vi.mock('../../services/dev-override.js')

const CommandConfig = new Config({root: __dirname})

describe('SandboxPreview', () => {
beforeEach(() => {
vi.mocked(devWithOverrideFile).mockResolvedValue(undefined)
})

async function run(argv: string[]) {
await CommandConfig.load()
const command = new SandboxPreview(argv, CommandConfig)
await command.run()
}

test('calls devWithOverrideFile in authless mock.shop mode', async () => {
await run(['--overrides=/path/to/overrides.json'])

expect(devWithOverrideFile).toHaveBeenCalledWith({
overrideJson: '/path/to/overrides.json',
open: true,
mockShop: true,
mockShopStorefrontUrl: undefined,
})
})

test('passes through --no-open when provided', async () => {
await run(['--overrides=/path/to/overrides.json', '--no-open'])

expect(devWithOverrideFile).toHaveBeenCalledWith({
overrideJson: '/path/to/overrides.json',
open: false,
mockShop: true,
mockShopStorefrontUrl: undefined,
})
})

test('passes through a custom storefront URL when provided', async () => {
await run(['--overrides=/path/to/overrides.json', '--storefront-url=http://localhost:3000'])

expect(devWithOverrideFile).toHaveBeenCalledWith({
overrideJson: '/path/to/overrides.json',
open: true,
mockShop: true,
mockShopStorefrontUrl: 'http://localhost:3000',
})
})
})
50 changes: 50 additions & 0 deletions packages/theme/src/cli/commands/theme/sandbox-preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {devWithOverrideFile} from '../../services/dev-override.js'
import {Flags} from '@oclif/core'
import Command from '@shopify/cli-kit/node/base-command'
import {globalFlags} from '@shopify/cli-kit/node/cli'

export default class SandboxPreview extends Command {
static summary = 'Opens a one-shot authless mock.shop sandbox preview from a JSON overrides file.'

static usage = ['theme sandbox-preview --overrides path/to/overrides.json']

static descriptionWithMarkdown = `Starts an authless, one-shot sandbox preview using [mock.shop](https://mock.shop/).

This prototype writes a local launcher page and opens it in your browser. The launcher then POSTs the override payload directly to the target storefront to render an initial preview.

- No store authentication is required
- No preview is persisted
- Navigation after the first page load will not preserve overrides`

static description = this.descriptionWithoutMarkdown()

static flags = {
...globalFlags,
overrides: Flags.string({
description: 'Path to a JSON overrides file.',
env: 'SHOPIFY_FLAG_OVERRIDES',
required: true,
}),
'no-open': Flags.boolean({
description: 'Do not automatically launch the local storefront preview launcher in your default web browser.',
env: 'SHOPIFY_FLAG_NO_OPEN',
default: false,
}),
'storefront-url': Flags.string({
description: 'Override the storefront preview target. Useful for local SFR testing.',
env: 'SHOPIFY_FLAG_STOREFRONT_URL',
required: false,
}),
}

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

await devWithOverrideFile({
overrideJson: flags.overrides,
open: !flags['no-open'],
mockShop: true,
mockShopStorefrontUrl: flags['storefront-url'],
})
}
}
87 changes: 87 additions & 0 deletions packages/theme/src/cli/services/dev-override.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import {devWithOverrideFile} from './dev-override.js'
import {openURLSafely} from './dev.js'
import {fetchDevServerSession} from '../utilities/theme-environment/dev-server-session.js'
import {createThemePreview, updateThemePreview} from '../utilities/theme-previews/preview.js'
import {startMockShopPreviewSession} from '../utilities/theme-previews/mock-shop.js'
import {describe, expect, test, vi} from 'vitest'
import {renderSuccess} from '@shopify/cli-kit/node/ui'
import {collectedLogs, clearCollectedLogs} from '@shopify/cli-kit/node/output'
import {fileExistsSync, readFile} from '@shopify/cli-kit/node/fs'

vi.mock('../utilities/theme-environment/dev-server-session.js')
vi.mock('../utilities/theme-previews/preview.js')
vi.mock('../utilities/theme-previews/mock-shop.js')
vi.mock('./dev.js', () => ({openURLSafely: vi.fn()}))
vi.mock('@shopify/cli-kit/node/ui')
vi.mock('@shopify/cli-kit/node/fs')
Expand All @@ -22,6 +24,9 @@ const mockSession = {
}
const expectedPreviewUrl = 'https://abc123.shopifypreview.com'
const expectedPreviewId = 'abc123'
const mockShopLauncherUrl = 'file:///tmp/mock-shop-preview.html'
const mockShopTargetUrl = 'https://demostore.mock.shop/?theme_preview'
const customMockShopTargetUrl = 'http://localhost:3000/?theme_preview'

describe('devWithOverrideFile', () => {
test('throws when override file does not exist', async () => {
Expand Down Expand Up @@ -208,4 +213,86 @@ describe('devWithOverrideFile', () => {
// Then
expect(renderSuccess).toHaveBeenCalled()
})

test('starts a one-shot mock.shop preview when mockShop is enabled', async () => {
vi.mocked(fileExistsSync).mockReturnValue(true)
vi.mocked(readFile).mockResolvedValue(Buffer.from(JSON.stringify({templates: {}})))
vi.mocked(startMockShopPreviewSession).mockResolvedValue({
launcherUrl: mockShopLauncherUrl,
targetUrl: mockShopTargetUrl,
completion: Promise.resolve(),
})

await devWithOverrideFile({adminSession, overrideJson: '/overrides.json', open: false, mockShop: true})

expect(startMockShopPreviewSession).toHaveBeenCalledWith(JSON.stringify({templates: {}}), {
storefrontUrl: undefined,
})
expect(fetchDevServerSession).not.toHaveBeenCalled()
expect(createThemePreview).not.toHaveBeenCalled()
expect(updateThemePreview).not.toHaveBeenCalled()
expect(renderSuccess).toHaveBeenCalledWith(
expect.objectContaining({
body: [
{
list: {
title: 'Mock.shop preview is ready',
items: [{link: {url: mockShopLauncherUrl}}, `Target: ${mockShopTargetUrl}`, 'This prototype opens an initial preview only.'],
},
},
],
}),
)
})

test('opens the launcher URL for a mock.shop preview', async () => {
vi.mocked(fileExistsSync).mockReturnValue(true)
vi.mocked(readFile).mockResolvedValue(Buffer.from(JSON.stringify({templates: {}})))
vi.mocked(startMockShopPreviewSession).mockResolvedValue({
launcherUrl: mockShopLauncherUrl,
targetUrl: mockShopTargetUrl,
completion: Promise.resolve(),
})

await devWithOverrideFile({adminSession, overrideJson: '/overrides.json', open: true, mockShop: true})

expect(openURLSafely).toHaveBeenCalledWith(mockShopLauncherUrl, 'mock.shop preview')
})

test('passes through a custom storefront URL in mock.shop mode', async () => {
vi.mocked(fileExistsSync).mockReturnValue(true)
vi.mocked(readFile).mockResolvedValue(Buffer.from(JSON.stringify({templates: {}})))
vi.mocked(startMockShopPreviewSession).mockResolvedValue({
launcherUrl: mockShopLauncherUrl,
targetUrl: customMockShopTargetUrl,
completion: Promise.resolve(),
})

await devWithOverrideFile({
adminSession,
overrideJson: '/overrides.json',
open: false,
mockShop: true,
mockShopStorefrontUrl: 'http://localhost:3000',
})

expect(startMockShopPreviewSession).toHaveBeenCalledWith(JSON.stringify({templates: {}}), {
storefrontUrl: 'http://localhost:3000',
})
})

test('rejects preview IDs in mock.shop mode', async () => {
vi.mocked(fileExistsSync).mockReturnValue(true)
vi.mocked(readFile).mockResolvedValue(Buffer.from(JSON.stringify({templates: {}})))

await expect(
devWithOverrideFile({
adminSession,
overrideJson: '/overrides.json',
open: false,
mockShop: true,
previewIdentifier: 'abc123',
}),
).rejects.toThrow('The --preview-id flag is not supported with --mock-shop.')
})
})
Loading
Loading