Skip to content

Commit e86c358

Browse files
committed
prototype easy stateless preview
1 parent 8d7074e commit e86c358

9 files changed

Lines changed: 495 additions & 3 deletions

File tree

packages/cli/oclif.manifest.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7979,6 +7979,71 @@
79797979
"strict": true,
79807980
"summary": "Renames an existing theme."
79817981
},
7982+
"theme:sandbox-preview": {
7983+
"aliases": [
7984+
],
7985+
"args": {
7986+
},
7987+
"customPluginName": "@shopify/theme",
7988+
"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",
7989+
"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",
7990+
"enableJsonFlag": false,
7991+
"flags": {
7992+
"no-color": {
7993+
"allowNo": false,
7994+
"description": "Disable color output.",
7995+
"env": "SHOPIFY_FLAG_NO_COLOR",
7996+
"hidden": false,
7997+
"name": "no-color",
7998+
"type": "boolean"
7999+
},
8000+
"no-open": {
8001+
"allowNo": false,
8002+
"description": "Do not automatically launch the local storefront preview launcher in your default web browser.",
8003+
"env": "SHOPIFY_FLAG_NO_OPEN",
8004+
"name": "no-open",
8005+
"type": "boolean"
8006+
},
8007+
"overrides": {
8008+
"description": "Path to a JSON overrides file.",
8009+
"env": "SHOPIFY_FLAG_OVERRIDES",
8010+
"hasDynamicHelp": false,
8011+
"multiple": false,
8012+
"name": "overrides",
8013+
"required": true,
8014+
"type": "option"
8015+
},
8016+
"storefront-url": {
8017+
"description": "Override the storefront preview target. Useful for local SFR testing.",
8018+
"env": "SHOPIFY_FLAG_STOREFRONT_URL",
8019+
"hasDynamicHelp": false,
8020+
"multiple": false,
8021+
"name": "storefront-url",
8022+
"required": false,
8023+
"type": "option"
8024+
},
8025+
"verbose": {
8026+
"allowNo": false,
8027+
"description": "Increase the verbosity of the output.",
8028+
"env": "SHOPIFY_FLAG_VERBOSE",
8029+
"hidden": false,
8030+
"name": "verbose",
8031+
"type": "boolean"
8032+
}
8033+
},
8034+
"hasDynamicHelp": false,
8035+
"hiddenAliases": [
8036+
],
8037+
"id": "theme:sandbox-preview",
8038+
"pluginAlias": "@shopify/cli",
8039+
"pluginName": "@shopify/cli",
8040+
"pluginType": "core",
8041+
"strict": true,
8042+
"summary": "Opens a one-shot authless mock.shop sandbox preview from a JSON overrides file.",
8043+
"usage": [
8044+
"theme sandbox-preview --overrides path/to/overrides.json"
8045+
]
8046+
},
79828047
"theme:serve": {
79838048
"aliases": [
79848049
],
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {devWithOverrideFile} from '../../../services/dev-override.js'
2+
import {Flags} from '@oclif/core'
3+
import Command from '@shopify/cli-kit/node/base-command'
4+
import {globalFlags} from '@shopify/cli-kit/node/cli'
5+
6+
export default class PreviewMock extends Command {
7+
static summary = 'Opens a one-shot authless mock.shop sandbox preview from a JSON overrides file.'
8+
9+
static usage = ['theme preview mock --overrides path/to/overrides.json']
10+
11+
static descriptionWithMarkdown = `Starts an authless, one-shot sandbox preview using [mock.shop](https://mock.shop/).
12+
13+
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.
14+
15+
- No store authentication is required
16+
- No preview is persisted
17+
- Navigation after the first page load will not preserve overrides`
18+
19+
static description = this.descriptionWithoutMarkdown()
20+
21+
static flags = {
22+
...globalFlags,
23+
overrides: Flags.string({
24+
description: 'Path to a JSON overrides file.',
25+
env: 'SHOPIFY_FLAG_OVERRIDES',
26+
required: true,
27+
}),
28+
open: Flags.boolean({
29+
description: 'Automatically launch the local storefront preview launcher in your default web browser.',
30+
env: 'SHOPIFY_FLAG_OPEN',
31+
default: true,
32+
}),
33+
'storefront-url': Flags.string({
34+
description: 'Override the storefront preview target. Useful for local SFR testing.',
35+
env: 'SHOPIFY_FLAG_STOREFRONT_URL',
36+
required: false,
37+
}),
38+
}
39+
40+
async run(): Promise<void> {
41+
const {flags} = await this.parse(PreviewMock)
42+
43+
await devWithOverrideFile({
44+
overrideJson: flags.overrides,
45+
open: flags.open,
46+
mockShop: true,
47+
mockShopStorefrontUrl: flags['storefront-url'],
48+
})
49+
}
50+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import SandboxPreview from './sandbox-preview.js'
2+
import {devWithOverrideFile} from '../../services/dev-override.js'
3+
import {Config} from '@oclif/core'
4+
import {describe, vi, expect, test, beforeEach} from 'vitest'
5+
6+
vi.mock('../../services/dev-override.js')
7+
8+
const CommandConfig = new Config({root: __dirname})
9+
10+
describe('SandboxPreview', () => {
11+
beforeEach(() => {
12+
vi.mocked(devWithOverrideFile).mockResolvedValue(undefined)
13+
})
14+
15+
async function run(argv: string[]) {
16+
await CommandConfig.load()
17+
const command = new SandboxPreview(argv, CommandConfig)
18+
await command.run()
19+
}
20+
21+
test('calls devWithOverrideFile in authless mock.shop mode', async () => {
22+
await run(['--overrides=/path/to/overrides.json'])
23+
24+
expect(devWithOverrideFile).toHaveBeenCalledWith({
25+
overrideJson: '/path/to/overrides.json',
26+
open: true,
27+
mockShop: true,
28+
mockShopStorefrontUrl: undefined,
29+
})
30+
})
31+
32+
test('passes through --no-open when provided', async () => {
33+
await run(['--overrides=/path/to/overrides.json', '--no-open'])
34+
35+
expect(devWithOverrideFile).toHaveBeenCalledWith({
36+
overrideJson: '/path/to/overrides.json',
37+
open: false,
38+
mockShop: true,
39+
mockShopStorefrontUrl: undefined,
40+
})
41+
})
42+
43+
test('passes through a custom storefront URL when provided', async () => {
44+
await run(['--overrides=/path/to/overrides.json', '--storefront-url=http://localhost:3000'])
45+
46+
expect(devWithOverrideFile).toHaveBeenCalledWith({
47+
overrideJson: '/path/to/overrides.json',
48+
open: true,
49+
mockShop: true,
50+
mockShopStorefrontUrl: 'http://localhost:3000',
51+
})
52+
})
53+
})
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {devWithOverrideFile} from '../../services/dev-override.js'
2+
import {Flags} from '@oclif/core'
3+
import Command from '@shopify/cli-kit/node/base-command'
4+
import {globalFlags} from '@shopify/cli-kit/node/cli'
5+
6+
export default class SandboxPreview extends Command {
7+
static summary = 'Opens a one-shot authless mock.shop sandbox preview from a JSON overrides file.'
8+
9+
static usage = ['theme sandbox-preview --overrides path/to/overrides.json']
10+
11+
static descriptionWithMarkdown = `Starts an authless, one-shot sandbox preview using [mock.shop](https://mock.shop/).
12+
13+
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.
14+
15+
- No store authentication is required
16+
- No preview is persisted
17+
- Navigation after the first page load will not preserve overrides`
18+
19+
static description = this.descriptionWithoutMarkdown()
20+
21+
static flags = {
22+
...globalFlags,
23+
overrides: Flags.string({
24+
description: 'Path to a JSON overrides file.',
25+
env: 'SHOPIFY_FLAG_OVERRIDES',
26+
required: true,
27+
}),
28+
'no-open': Flags.boolean({
29+
description: 'Do not automatically launch the local storefront preview launcher in your default web browser.',
30+
env: 'SHOPIFY_FLAG_NO_OPEN',
31+
default: false,
32+
}),
33+
'storefront-url': Flags.string({
34+
description: 'Override the storefront preview target. Useful for local SFR testing.',
35+
env: 'SHOPIFY_FLAG_STOREFRONT_URL',
36+
required: false,
37+
}),
38+
}
39+
40+
async run(): Promise<void> {
41+
const {flags} = await this.parse(SandboxPreview)
42+
43+
await devWithOverrideFile({
44+
overrideJson: flags.overrides,
45+
open: !flags['no-open'],
46+
mockShop: true,
47+
mockShopStorefrontUrl: flags['storefront-url'],
48+
})
49+
}
50+
}

packages/theme/src/cli/services/dev-override.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import {devWithOverrideFile} from './dev-override.js'
22
import {openURLSafely} from './dev.js'
33
import {fetchDevServerSession} from '../utilities/theme-environment/dev-server-session.js'
44
import {createThemePreview, updateThemePreview} from '../utilities/theme-previews/preview.js'
5+
import {startMockShopPreviewSession} from '../utilities/theme-previews/mock-shop.js'
56
import {describe, expect, test, vi} from 'vitest'
67
import {renderSuccess} from '@shopify/cli-kit/node/ui'
78
import {collectedLogs, clearCollectedLogs} from '@shopify/cli-kit/node/output'
89
import {fileExistsSync, readFile} from '@shopify/cli-kit/node/fs'
910

1011
vi.mock('../utilities/theme-environment/dev-server-session.js')
1112
vi.mock('../utilities/theme-previews/preview.js')
13+
vi.mock('../utilities/theme-previews/mock-shop.js')
1214
vi.mock('./dev.js', () => ({openURLSafely: vi.fn()}))
1315
vi.mock('@shopify/cli-kit/node/ui')
1416
vi.mock('@shopify/cli-kit/node/fs')
@@ -22,6 +24,9 @@ const mockSession = {
2224
}
2325
const expectedPreviewUrl = 'https://abc123.shopifypreview.com'
2426
const expectedPreviewId = 'abc123'
27+
const mockShopLauncherUrl = 'file:///tmp/mock-shop-preview.html'
28+
const mockShopTargetUrl = 'https://demostore.mock.shop/?theme_preview'
29+
const customMockShopTargetUrl = 'http://localhost:3000/?theme_preview'
2530

2631
describe('devWithOverrideFile', () => {
2732
test('throws when override file does not exist', async () => {
@@ -208,4 +213,86 @@ describe('devWithOverrideFile', () => {
208213
// Then
209214
expect(renderSuccess).toHaveBeenCalled()
210215
})
216+
217+
test('starts a one-shot mock.shop preview when mockShop is enabled', async () => {
218+
vi.mocked(fileExistsSync).mockReturnValue(true)
219+
vi.mocked(readFile).mockResolvedValue(Buffer.from(JSON.stringify({templates: {}})))
220+
vi.mocked(startMockShopPreviewSession).mockResolvedValue({
221+
launcherUrl: mockShopLauncherUrl,
222+
targetUrl: mockShopTargetUrl,
223+
completion: Promise.resolve(),
224+
})
225+
226+
await devWithOverrideFile({adminSession, overrideJson: '/overrides.json', open: false, mockShop: true})
227+
228+
expect(startMockShopPreviewSession).toHaveBeenCalledWith(JSON.stringify({templates: {}}), {
229+
storefrontUrl: undefined,
230+
})
231+
expect(fetchDevServerSession).not.toHaveBeenCalled()
232+
expect(createThemePreview).not.toHaveBeenCalled()
233+
expect(updateThemePreview).not.toHaveBeenCalled()
234+
expect(renderSuccess).toHaveBeenCalledWith(
235+
expect.objectContaining({
236+
body: [
237+
{
238+
list: {
239+
title: 'Mock.shop preview is ready',
240+
items: [{link: {url: mockShopLauncherUrl}}, `Target: ${mockShopTargetUrl}`, 'This prototype opens an initial preview only.'],
241+
},
242+
},
243+
],
244+
}),
245+
)
246+
})
247+
248+
test('opens the launcher URL for a mock.shop preview', async () => {
249+
vi.mocked(fileExistsSync).mockReturnValue(true)
250+
vi.mocked(readFile).mockResolvedValue(Buffer.from(JSON.stringify({templates: {}})))
251+
vi.mocked(startMockShopPreviewSession).mockResolvedValue({
252+
launcherUrl: mockShopLauncherUrl,
253+
targetUrl: mockShopTargetUrl,
254+
completion: Promise.resolve(),
255+
})
256+
257+
await devWithOverrideFile({adminSession, overrideJson: '/overrides.json', open: true, mockShop: true})
258+
259+
expect(openURLSafely).toHaveBeenCalledWith(mockShopLauncherUrl, 'mock.shop preview')
260+
})
261+
262+
test('passes through a custom storefront URL in mock.shop mode', async () => {
263+
vi.mocked(fileExistsSync).mockReturnValue(true)
264+
vi.mocked(readFile).mockResolvedValue(Buffer.from(JSON.stringify({templates: {}})))
265+
vi.mocked(startMockShopPreviewSession).mockResolvedValue({
266+
launcherUrl: mockShopLauncherUrl,
267+
targetUrl: customMockShopTargetUrl,
268+
completion: Promise.resolve(),
269+
})
270+
271+
await devWithOverrideFile({
272+
adminSession,
273+
overrideJson: '/overrides.json',
274+
open: false,
275+
mockShop: true,
276+
mockShopStorefrontUrl: 'http://localhost:3000',
277+
})
278+
279+
expect(startMockShopPreviewSession).toHaveBeenCalledWith(JSON.stringify({templates: {}}), {
280+
storefrontUrl: 'http://localhost:3000',
281+
})
282+
})
283+
284+
test('rejects preview IDs in mock.shop mode', async () => {
285+
vi.mocked(fileExistsSync).mockReturnValue(true)
286+
vi.mocked(readFile).mockResolvedValue(Buffer.from(JSON.stringify({templates: {}})))
287+
288+
await expect(
289+
devWithOverrideFile({
290+
adminSession,
291+
overrideJson: '/overrides.json',
292+
open: false,
293+
mockShop: true,
294+
previewIdentifier: 'abc123',
295+
}),
296+
).rejects.toThrow('The --preview-id flag is not supported with --mock-shop.')
297+
})
211298
})

0 commit comments

Comments
 (0)