Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/theme-dev-reconciliation-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/theme': patch
---

Add a `--reconciliation-strategy` option to `theme dev`.
6 changes: 6 additions & 0 deletions docs-shopify.dev/commands/interfaces/theme-dev.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ export interface themedev {
*/
'--port <value>'?: string

/**
* How to resolve JSON conflicts when --theme-editor-sync is enabled. Use keep-local to keep local files, keep-remote to keep remote files, or abort to fail instead of prompting.
* @environment SHOPIFY_FLAG_RECONCILIATION_STRATEGY
*/
'--reconciliation-strategy <value>'?: string

/**
* Inject the standard events inspector into storefront HTML.
* @environment SHOPIFY_FLAG_STANDARD_EVENTS_INSPECTOR
Expand Down
11 changes: 10 additions & 1 deletion docs-shopify.dev/generated/generated_docs_data_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -5095,6 +5095,15 @@
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_PORT"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/theme-dev.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--reconciliation-strategy <value>",
"value": "string",
"description": "How to resolve JSON conflicts when --theme-editor-sync is enabled. Use keep-local to keep local files, keep-remote to keep remote files, or abort to fail instead of prompting.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_RECONCILIATION_STRATEGY"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/theme-dev.interface.ts",
"syntaxKind": "PropertySignature",
Expand Down Expand Up @@ -5195,7 +5204,7 @@
"environmentValue": "SHOPIFY_FLAG_IGNORE"
}
],
"value": "export interface themedev {\n /**\n * Allow development on a live theme.\n * @environment SHOPIFY_FLAG_ALLOW_LIVE\n */\n '-a, --allow-live'?: ''\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Controls the visibility of the error overlay when an theme asset upload fails:\n- silent Prevents the error overlay from appearing.\n- default Displays the error overlay.\n \n * @environment SHOPIFY_FLAG_ERROR_OVERLAY\n */\n '--error-overlay <value>'?: string\n\n /**\n * Set which network interface the web server listens on. The default value is 127.0.0.1.\n * @environment SHOPIFY_FLAG_HOST\n */\n '--host <value>'?: string\n\n /**\n * Skip hot reloading any files that match the specified pattern.\n * @environment SHOPIFY_FLAG_IGNORE\n */\n '-x, --ignore <value>'?: string\n\n /**\n * The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.\n * @environment SHOPIFY_FLAG_LISTING\n */\n '--listing <value>'?: string\n\n /**\n * The live reload mode switches the server behavior when a file is modified:\n- hot-reload Hot reloads local changes to CSS and sections (default)\n- full-page Always refreshes the entire page\n- off Deactivate live reload\n * @environment SHOPIFY_FLAG_LIVE_RELOAD\n */\n '--live-reload <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Prevents files from being deleted in the remote theme when a file has been deleted locally. This applies to files that are deleted while the command is running, and files that have been deleted locally before the command is run.\n * @environment SHOPIFY_FLAG_NODELETE\n */\n '-n, --nodelete'?: ''\n\n /**\n * The file path or URL. The file path is to a file that you want updated on idle. The URL path is where you want a webhook posted to report on file changes.\n * @environment SHOPIFY_FLAG_NOTIFY\n */\n '--notify <value>'?: string\n\n /**\n * Hot reload only files that match the specified pattern.\n * @environment SHOPIFY_FLAG_ONLY\n */\n '-o, --only <value>'?: string\n\n /**\n * Automatically launch the theme preview in your default web browser.\n * @environment SHOPIFY_FLAG_OPEN\n */\n '--open'?: ''\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password <value>'?: string\n\n /**\n * The path where you want to run the command. Defaults to the current working directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Local port to serve theme preview from. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_PORT\n */\n '--port <value>'?: string\n\n /**\n * Inject the standard events inspector into storefront HTML.\n * @environment SHOPIFY_FLAG_STANDARD_EVENTS_INSPECTOR\n */\n '--standard-events-inspector'?: ''\n\n /**\n * Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>'?: string\n\n /**\n * The password for storefronts with password protection.\n * @environment SHOPIFY_FLAG_STORE_PASSWORD\n */\n '--store-password <value>'?: string\n\n /**\n * Theme ID or name of the remote theme.\n * @environment SHOPIFY_FLAG_THEME_ID\n */\n '-t, --theme <value>'?: string\n\n /**\n * Synchronize Theme Editor updates in the local theme files.\n * @environment SHOPIFY_FLAG_THEME_EDITOR_SYNC\n */\n '--theme-editor-sync'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
"value": "export interface themedev {\n /**\n * Allow development on a live theme.\n * @environment SHOPIFY_FLAG_ALLOW_LIVE\n */\n '-a, --allow-live'?: ''\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Controls the visibility of the error overlay when an theme asset upload fails:\n- silent Prevents the error overlay from appearing.\n- default Displays the error overlay.\n \n * @environment SHOPIFY_FLAG_ERROR_OVERLAY\n */\n '--error-overlay <value>'?: string\n\n /**\n * Set which network interface the web server listens on. The default value is 127.0.0.1.\n * @environment SHOPIFY_FLAG_HOST\n */\n '--host <value>'?: string\n\n /**\n * Skip hot reloading any files that match the specified pattern.\n * @environment SHOPIFY_FLAG_IGNORE\n */\n '-x, --ignore <value>'?: string\n\n /**\n * The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.\n * @environment SHOPIFY_FLAG_LISTING\n */\n '--listing <value>'?: string\n\n /**\n * The live reload mode switches the server behavior when a file is modified:\n- hot-reload Hot reloads local changes to CSS and sections (default)\n- full-page Always refreshes the entire page\n- off Deactivate live reload\n * @environment SHOPIFY_FLAG_LIVE_RELOAD\n */\n '--live-reload <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Prevents files from being deleted in the remote theme when a file has been deleted locally. This applies to files that are deleted while the command is running, and files that have been deleted locally before the command is run.\n * @environment SHOPIFY_FLAG_NODELETE\n */\n '-n, --nodelete'?: ''\n\n /**\n * The file path or URL. The file path is to a file that you want updated on idle. The URL path is where you want a webhook posted to report on file changes.\n * @environment SHOPIFY_FLAG_NOTIFY\n */\n '--notify <value>'?: string\n\n /**\n * Hot reload only files that match the specified pattern.\n * @environment SHOPIFY_FLAG_ONLY\n */\n '-o, --only <value>'?: string\n\n /**\n * Automatically launch the theme preview in your default web browser.\n * @environment SHOPIFY_FLAG_OPEN\n */\n '--open'?: ''\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password <value>'?: string\n\n /**\n * The path where you want to run the command. Defaults to the current working directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Local port to serve theme preview from. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_PORT\n */\n '--port <value>'?: string\n\n /**\n * How to resolve JSON conflicts when --theme-editor-sync is enabled. Use keep-local to keep local files, keep-remote to keep remote files, or abort to fail instead of prompting.\n * @environment SHOPIFY_FLAG_RECONCILIATION_STRATEGY\n */\n '--reconciliation-strategy <value>'?: string\n\n /**\n * Inject the standard events inspector into storefront HTML.\n * @environment SHOPIFY_FLAG_STANDARD_EVENTS_INSPECTOR\n */\n '--standard-events-inspector'?: ''\n\n /**\n * Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>'?: string\n\n /**\n * The password for storefronts with password protection.\n * @environment SHOPIFY_FLAG_STORE_PASSWORD\n */\n '--store-password <value>'?: string\n\n /**\n * Theme ID or name of the remote theme.\n * @environment SHOPIFY_FLAG_THEME_ID\n */\n '-t, --theme <value>'?: string\n\n /**\n * Synchronize Theme Editor updates in the local theme files.\n * @environment SHOPIFY_FLAG_THEME_EDITOR_SYNC\n */\n '--theme-editor-sync'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
}
},
"themeduplicate": {
Expand Down
10 changes: 8 additions & 2 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2511,8 +2511,9 @@ Uploads the current theme as a development theme to the connected store, then pr
USAGE
$ shopify theme dev [-a] [-e <value>...] [--error-overlay silent|default] [--host <value>] [-x <value>...]
[--listing <value>] [--live-reload hot-reload|full-page|off] [--no-color] [-n] [--notify <value>] [-o <value>...]
[--open] [--password <value>] [--path <value>] [--port <value>] [--standard-events-inspector] [-s <value>]
[--store-password <value>] [-t <value>] [--theme-editor-sync] [--verbose]
[--open] [--password <value>] [--path <value>] [--port <value>] [--reconciliation-strategy
keep-local|keep-remote|abort] [--standard-events-inspector] [-s <value>] [--store-password <value>] [-t <value>]
[--theme-editor-sync] [--verbose]
FLAGS
-a, --allow-live
Expand Down Expand Up @@ -2581,6 +2582,11 @@ FLAGS
--port=<value>
[env: SHOPIFY_FLAG_PORT] Local port to serve theme preview from. Must be between 1 and 65535.
--reconciliation-strategy=<option>
[env: SHOPIFY_FLAG_RECONCILIATION_STRATEGY] How to resolve JSON conflicts when --theme-editor-sync is enabled. Use
keep-local to keep local files, keep-remote to keep remote files, or abort to fail instead of prompting.
<options: keep-local|keep-remote|abort>
--standard-events-inspector
[env: SHOPIFY_FLAG_STANDARD_EVENTS_INSPECTOR] Inject the standard events inspector into storefront HTML.
Expand Down
13 changes: 13 additions & 0 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7172,6 +7172,19 @@
"name": "port",
"type": "option"
},
"reconciliation-strategy": {
"description": "How to resolve JSON conflicts when --theme-editor-sync is enabled. Use keep-local to keep local files, keep-remote to keep remote files, or abort to fail instead of prompting.",
"env": "SHOPIFY_FLAG_RECONCILIATION_STRATEGY",
"hasDynamicHelp": false,
"multiple": false,
"name": "reconciliation-strategy",
"options": [
"keep-local",
"keep-remote",
"abort"
],
"type": "option"
},
"standard-events-inspector": {
"allowNo": false,
"description": "Inject the standard events inspector into storefront HTML.",
Expand Down
9 changes: 8 additions & 1 deletion packages/theme/src/cli/commands/theme/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {recordEvent} from '@shopify/cli-kit/node/analytics'
import {AdminSession} from '@shopify/cli-kit/node/session'
import {InferredFlags} from '@oclif/core/interfaces'

import type {ErrorOverlayMode, LiveReload} from '../../utilities/theme-environment/types.js'
import type {ErrorOverlayMode, LiveReload, ReconciliationStrategy} from '../../utilities/theme-environment/types.js'

type DevFlags = InferredFlags<typeof Dev.flags>

Expand Down Expand Up @@ -82,6 +82,12 @@ You can run this command only in a directory that matches the [default Shopify t
env: 'SHOPIFY_FLAG_STANDARD_EVENTS_INSPECTOR',
default: false,
}),
'reconciliation-strategy': Flags.string({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theme dev --reconciliation-strategy keep-local on its own does nothing and emits no warning, shall we add dependency on theme-editor-sync?

Suggestion: Consider declaring the dependency so oclif surfaces an error instead of silently ignoring the flag:

Suggested change
'reconciliation-strategy': Flags.string({
'reconciliation-strategy': Flags.string({
description:
'How to resolve JSON conflicts when --theme-editor-sync is enabled. Use keep-local to keep local files, keep-remote to keep remote files, or abort to fail instead of prompting.',
options: ['keep-local', 'keep-remote', 'abort'],
env: 'SHOPIFY_FLAG_RECONCILIATION_STRATEGY',
dependsOn: ['theme-editor-sync'],
}),

description:
'How to resolve JSON conflicts when --theme-editor-sync is enabled. Use keep-local to keep local files, keep-remote to keep remote files, or abort to fail instead of prompting.',
options: ['keep-local', 'keep-remote', 'abort'],
env: 'SHOPIFY_FLAG_RECONCILIATION_STRATEGY',
}),
port: portFlag({
description: 'Local port to serve theme preview from.',
env: 'SHOPIFY_FLAG_PORT',
Expand Down Expand Up @@ -186,6 +192,7 @@ You can run this command only in a directory that matches the [default Shopify t
open: flags.open,
'theme-editor-sync': flags['theme-editor-sync'],
'standard-events-inspector': flags['standard-events-inspector'],
reconciliationStrategy: flags['reconciliation-strategy'] as ReconciliationStrategy | undefined,
noDelete: flags.nodelete,
ignore,
only,
Expand Down
9 changes: 8 additions & 1 deletion packages/theme/src/cli/services/dev.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import {hasRequiredThemeDirectories, mountThemeFileSystem} from '../utilities/theme-fs.js'
import {ensureDirectoryConfirmed} from '../utilities/theme-ui.js'
import {setupDevServer} from '../utilities/theme-environment/theme-environment.js'
import {DevServerContext, ErrorOverlayMode, LiveReload} from '../utilities/theme-environment/types.js'
import {
DevServerContext,
ErrorOverlayMode,
LiveReload,
ReconciliationStrategy,
} from '../utilities/theme-environment/types.js'
import {isStorefrontPasswordProtected} from '../utilities/theme-environment/storefront-session.js'
import {ensureValidPassword} from '../utilities/theme-environment/storefront-password-prompt.js'
import {emptyThemeExtFileSystem} from '../utilities/theme-fs-empty.js'
Expand Down Expand Up @@ -41,6 +46,7 @@ interface DevOptions {
force: boolean
'standard-events-inspector': boolean
'theme-editor-sync': boolean
reconciliationStrategy?: ReconciliationStrategy
'live-reload': LiveReload
'error-overlay': ErrorOverlayMode
noDelete: boolean
Expand Down Expand Up @@ -129,6 +135,7 @@ export async function dev(options: DevOptions) {
lastRequestedPath: '',
options: {
themeEditorSync: options['theme-editor-sync'],
reconciliationStrategy: options.reconciliationStrategy,
host,
port,
open: options.open,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {outputDebug} from '@shopify/cli-kit/node/output'
import {AdminSession} from '@shopify/cli-kit/node/session'
import {Checksum, Theme, ThemeFileSystem} from '@shopify/cli-kit/node/themes/types'
import {fetchChecksums} from '@shopify/cli-kit/node/themes/api'
import type {ReconciliationStrategy} from './types.js'

export const LOCAL_STRATEGY = 'local'
export const REMOTE_STRATEGY = 'remote'
Expand All @@ -21,6 +22,7 @@ export async function reconcileAndPollThemeEditorChanges(
noDelete: boolean
ignore: string[]
only: string[]
reconciliationStrategy?: ReconciliationStrategy
},
rejectBackgroundJob: (reason?: unknown) => void,
): Promise<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ describe('setupDevServer', () => {
context.session,
[],
context.localThemeFileSystem,
{noDelete: true, ...filters},
{noDelete: true, ...filters, reconciliationStrategy: undefined},
expect.anything(),
)
// This is the best way I could think of verifying the rejectBackgroundJob
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ function handleThemeEditorSync(
noDelete: ctx.options.noDelete,
ignore: ctx.options.ignore,
only: ctx.options.only,
reconciliationStrategy: ctx.options.reconciliationStrategy,
},
rejectBackgroundJob,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,26 @@ describe('reconcileJsonFiles', () => {
})

describe('files only present on remote developer theme', () => {
test('should download assets from remote theme without prompting when keep-remote strategy is selected', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check that local files do get deleted

// Given
const assetToBeDownloaded = {checksum: '2', key: 'templates/second_asset.json', value: 'content'}
vi.mocked(fetchThemeAssets).mockResolvedValue([assetToBeDownloaded])

// When
await reconcileAndWaitForReconciliationFinish(
developmentTheme,
adminSession,
[assetToBeDownloaded],
defaultThemeFileSystem,
{...defaultOptions, reconciliationStrategy: 'keep-remote'},
)

// Then
expect(renderSelectPrompt).not.toHaveBeenCalled()
expect(fetchThemeAssets).toHaveBeenCalledWith(developmentTheme.id, [assetToBeDownloaded.key], adminSession)
expect(defaultThemeFileSystem.files.get('templates/second_asset.json')).toEqual(assetToBeDownloaded)
})

test('should download assets from remote theme when `remote` source is selected', async () => {
// Given
vi.mocked(renderSelectPrompt).mockResolvedValue(REMOTE_STRATEGY)
Expand Down Expand Up @@ -220,6 +240,46 @@ describe('reconcileJsonFiles', () => {
})

describe('files with conflicting checksums', () => {
test('should keep local files without prompting when keep-local strategy is selected', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly check than remote files are deleted

// Given
const files = new Map([['templates/asset.json', {checksum: '1', key: 'templates/asset.json'}]])
const localThemeFileSystem = fakeThemeFileSystem('tmp', files)
const remoteChecksums = [{checksum: '2', key: 'templates/asset.json'}]

// When
await reconcileAndWaitForReconciliationFinish(
developmentTheme,
adminSession,
remoteChecksums,
localThemeFileSystem,
{...defaultOptions, reconciliationStrategy: 'keep-local'},
)

// Then
expect(renderSelectPrompt).not.toHaveBeenCalled()
expect(fetchThemeAssets).not.toHaveBeenCalled()
})

test('should abort without prompting when abort strategy is selected', async () => {
// Given
const files = new Map([['templates/asset.json', {checksum: '1', key: 'templates/asset.json'}]])
const localThemeFileSystem = fakeThemeFileSystem('tmp', files)
const remoteChecksums = [{checksum: '2', key: 'templates/asset.json'}]

// When
const result = reconcileAndWaitForReconciliationFinish(
developmentTheme,
adminSession,
remoteChecksums,
localThemeFileSystem,
{...defaultOptions, reconciliationStrategy: 'abort'},
)

// Then
await expect(result).rejects.toThrow('Theme JSON files need reconciliation.')
expect(renderSelectPrompt).not.toHaveBeenCalled()
})

test('should download files from remote theme when `remote` source is selected', async () => {
// Given
vi.mocked(renderSelectPrompt).mockResolvedValue(REMOTE_STRATEGY)
Expand Down
Loading
Loading