Skip to content

[Feature] Add Theme previews via JSON#6890

Merged
dengjeffrey merged 2 commits intomainfrom
jd/quick-preview
Mar 18, 2026
Merged

[Feature] Add Theme previews via JSON#6890
dengjeffrey merged 2 commits intomainfrom
jd/quick-preview

Conversation

@dengjeffrey
Copy link
Copy Markdown
Contributor

@dengjeffrey dengjeffrey commented Feb 24, 2026

WHY are these changes introduced?

Resolves https://github.com/shop/issues-growth-activation/issues/1902

WHAT is this pull request doing?

  1. Adds theme preview to support temporarily overriding a theme via JSON and being able to preview it

More internal context: https://docs.google.com/document/d/13sa0iRs_DSgyPHbx9IIF64rvhh_KZCDLfJgwpSIxP4A/edit?usp=sharing

--override specifies the path to the override JSON to process
--preview-id specifies an existing preview session to be updated with the JSON value in source

How to test your changes?

https://docs.google.com/document/d/13sa0iRs_DSgyPHbx9IIF64rvhh_KZCDLfJgwpSIxP4A/edit?tab=t.0#bookmark=id.rhjcmeq18hn7

Post-release steps

Measuring impact

How do we know this change was effective? Please choose one:

  • n/a - this doesn't need measurement, e.g. a linting rule or a bug-fix
  • Existing analytics will cater for this addition
  • PR includes analytics changes to measure impact

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 24, 2026

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 82.22% 14928/18157
🟡 Branches 74.66% 7394/9904
🟢 Functions 81.36% 3767/4630
🟢 Lines 82.61% 14115/17086

Test suite run success

3910 tests passing in 1502 suites.

Report generated by 🧪jest coverage report action from 962e932

@dengjeffrey dengjeffrey force-pushed the jd/quick-preview branch 4 times, most recently from 06454b5 to f131575 Compare February 25, 2026 02:47
@dengjeffrey dengjeffrey changed the title Jd/quick preview [Feature] Add Theme previews via JSON Feb 25, 2026
devFlags.path = resolvedSource
} else {
recordEvent('theme-command:dev:override-session')
await createOverrideDevSession(resolvedSource, devFlags, adminSession)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Using this same pattern here to "create" a dev session even if we don't have a long running process yet. This will make it trivial to add hot reloading.

@dengjeffrey dengjeffrey requested a review from karreiro February 25, 2026 17:57
@dengjeffrey dengjeffrey marked this pull request as ready for review February 25, 2026 18:03
@dengjeffrey dengjeffrey requested review from a team as code owners February 25, 2026 18:03
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@karreiro karreiro left a comment

Choose a reason for hiding this comment

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

Thank you for this PR, @dengjeffrey!

This feature is going to be really useful for developers — it feels like something they could use as a building block to create interesting things on top of.

I've left some comments. Once we tophat this, I'll revisit the review, try things end-to-end, and approve.

Thanks again!

}

const fileContent = await readFile(options.overrideJson)
const overrides = JSON.parse(fileContent) as ThemeOverrides
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.

May we handle JSON parser errors here?

description:
'The source for the dev server. Can be a directory or a JSON overrides file. When a directory is provided, it is used as the theme directory. When a JSON file is provided, overrides are applied to the theme specified by --theme.',
env: 'SHOPIFY_FLAG_SOURCE',
}),
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.

While reviewing the flags for this command, I noticed we already have --path, so there are two options:

  1. When --source is empty, document that the command uses the file system as the source and relies on --path.
  2. Remove --source in favor of --path, allowing you to pass either a --path or an override file as a parameter.

I think option 2 is the cleaner approach. What do you think?

Comment on lines +150 to +154
'preview-id': Flags.string({
description:
'An existing preview identifier to update instead of creating a new preview. Used with --source when pointing to a JSON overrides file.',
env: 'SHOPIFY_FLAG_PREVIEW_ID',
}),
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.

When reading this as a user, I immediately wondered how I would get a --preview-id. I think we could mention that --preview-id is an optional field that you may get from previous preview runs.

/**
* A scope supported by the Storefront Renderer API.
*/
export type StorefrontRendererScope = 'devtools'
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.

Deferring review of this line to the @Shopify/app-inner-loop team, as they implemented the storefront renderer scopes and have more context on this area.

overridesContent,
}: CreateThemePreviewOptions): Promise<ThemePreviewResult> {
recordTiming('theme-preview:create')
const baseUrl = buildBaseStorefrontUrl(session)
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.

Did you have a chance to tophat this using the Theme Access app?

I wonder if changes will be required there to support the new API call. I think it should work out of the box, but worth double-checking (specially from scopes perspective).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will do! Now that everything required to test this has been merged, I will give it a go tomorrow.

...defaultHeaders(),
Authorization: `Bearer ${storefrontToken}`,
'Content-Type': 'application/json',
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cookies are incorrectly spread into headers instead of being sent as a Cookie header

The code spreads cookie key/values directly into headers:

headers: {
  ...session.sessionCookies,
  ...defaultHeaders(),
  Authorization: `Bearer ${storefrontToken}`,
  'Content-Type': 'application/json',
}

If session.sessionCookies is shaped like { storefront_digest: '...', _shopify_essential: '...' }, this produces custom headers named storefront_digest and _shopify_essential rather than a valid Cookie header. Servers may ignore these, causing preview creation/update to fail with auth/session errors.

@binks-code-reviewer
Copy link
Copy Markdown

binks-code-reviewer Bot commented Feb 26, 2026

🤖 Code Review · #projects-dev-ai for questions
React with 👍/👎 or reply — all feedback helps improve the agent.

Complete - No issues

📋 History

✅ 1 findings → ✅ 2 findings → ✅ No issues → ✅ No issues → ❌ Failed → ✅ No issues → ✅ 1 findings → ✅ No issues

recordEvent('theme-command:dev:override-session')
await createOverrideDevSession(devFlags.path, devFlags, adminSession)
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

--path non-directory values are treated as JSON mode (breaks existing usage when path is missing/invalid)

The command switches to override-preview mode whenever isDirectorySync(devFlags.path) is false:

if (!isDirectorySync(devFlags.path)) {
  recordEvent('theme-command:dev:override-session')
  await createOverrideDevSession(devFlags.path, devFlags, adminSession)
  return
}

This will also be true for non-existent directories, non-JSON file paths, and possibly undefined/empty values depending on defaults. In those cases, the command incorrectly requires --theme and attempts JSON parsing, producing confusing errors instead of the existing “directory not found” / “not a theme directory” behavior.

metadata: {
...existingMetadata,
theme_id: themeId,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

constructOverrides may throw or behave unexpectedly if metadata is not an object

constructOverrides assumes overrides.metadata is object-like:

const existingMetadata = (overrides.metadata ?? {}) as ThemeOverrides
// ...
metadata: {
  ...existingMetadata,
  theme_id: themeId,
}

If the override JSON has "metadata": "v1" or "metadata": [], spreading can throw or produce odd results (arrays spread into numeric keys).

@dengjeffrey dengjeffrey requested a review from karreiro February 27, 2026 02:23
Comment thread packages/cli-kit/src/private/node/session/scopes.ts
Comment on lines +55 to +59
path: {
...themeFlags.path,
description:
'The path for the dev server. It can be a directory or a JSON overrides file. When a directory is provided, it is used as the theme directory. When a JSON file is provided, overrides are applied to the theme specified by --theme. Defaults to the current working directory.',
} as typeof themeFlags.path,
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.

path is a globally used flag and should keep its behaviour consistent over all commands (apps, theme...).
path means: execute the command in the given path instead of in the current directory.
It's not in globalFlags because not all commands are executed in a specific path, but maybe it should.

Anyway, I don't think we should change how path works in this specific command only. It'd be better to have a new flag

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll move it out to an --overrides flag

@dengjeffrey dengjeffrey force-pushed the jd/quick-preview branch 3 times, most recently from 65a7479 to c9446c1 Compare February 27, 2026 23:14
Copy link
Copy Markdown
Contributor

@karreiro karreiro left a comment

Choose a reason for hiding this comment

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

Thank you for the changes, @dengjeffrey!

I've tried to tophat and I'm getting a 401, even tho I'm authenticated (as we may notice in the shopify theme list command):

Image

@dengjeffrey
Copy link
Copy Markdown
Contributor Author

@karreiro thanks for taking a look! Yea there were some changes to Identity. I'll DM you

}: CreateThemePreviewOptions): Promise<ThemePreviewResult> {
recordTiming('theme-preview:create')
const baseUrl = buildBaseStorefrontUrl(session)
const url = `${baseUrl}/theme_preview.json?preview_theme_id=${themeId}`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👍

Comment thread packages/theme/src/cli/utilities/theme-previews/preview.ts
Comment thread .changeset/slick-humans-enjoy.md Outdated
'@shopify/cli': minor
---

Add support for theme previews using an override via `theme dev`. --path now supports an override JSON. A new --preview-id flag is also introduced to handle updates for preview.
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.

I think this changeset is outdated

@dengjeffrey dengjeffrey force-pushed the jd/quick-preview branch 3 times, most recently from 9e0a893 to f8dbc94 Compare March 13, 2026 17:54
@dengjeffrey dengjeffrey requested a review from isaacroldan March 13, 2026 21:36
Copy link
Copy Markdown
Contributor

@frandiox frandiox left a comment

Choose a reason for hiding this comment

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

Hey 👋 I've left some comments to understand better this PR.

A few more things:

  • Do we have a schema to validate the overrides JSON? I know we are going to document it but I think quick validation and showing a correct shape in the command it self might be the best way to inform the developer on how to use it.
  • How are these JSON files going to be created? Manually? Or do we intend to provide some sort of theme diff command/utility to create it?

const fileContent = await readFile(options.overrideJson)
let overrides: ThemeOverrides
try {
overrides = JSON.parse(fileContent) as ThemeOverrides
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.

Should we also validate here the size limit and the expected JSON shape? Or are we going to rely solely on server validation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Solely rely on server validation so we don't repeat logic

Comment on lines +162 to +166
if (devFlags.overrides) {
recordEvent('theme-command:dev:override-session')
await createOverrideDevSession(devFlags.overrides, devFlags, adminSession)
return
}
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.

This is rather counter-intuitive for me. I would expect a theme dev command to be a long running server that updates when I change something in my source files, not a one-off override from a separate file.

In my opinion, this seems more aligned with theme share (shopify theme share --overrides changes.json). Even with file watching added later, theme dev comes with a lot of irrelevant baggage (local proxy server, HMR, port/host flags, etc) that doesn't apply to this flow. A --watch flag on a simpler command would achieve the same result without the semantic mismatch 🤔

Is there any strong reason we need this to be in theme dev specifically? Are we thinking of adding extra functionality in the future that makes this play well with theme dev?

The main mismatch I see with theme share is that this command currently creates a new theme entry in Admin, and it wouldn't do so with --overrides. But it stills feels closer semantically than theme dev. In any case, not a strong opposition if we want to keep going with theme dev.

@dengjeffrey dengjeffrey force-pushed the jd/quick-preview branch 4 times, most recently from 24b9e88 to ce60476 Compare March 17, 2026 23:38
Copy link
Copy Markdown
Contributor

@frandiox frandiox left a comment

Choose a reason for hiding this comment

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

LGTM! Tested with normal auth and also with theme access app, it works 🎉 Thanks!

Comment on lines +56 to +65
renderSuccess({
body: [
{
list: {
title: options.previewIdentifier ? 'Preview updated' : 'Preview is ready',
items: [{link: {url: preview.url}}, `Preview ID: ${preview.preview_identifier}`],
},
},
],
})
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.

To make this easily usable by agents, perhaps we could add --json flag support for structured output instead of a banner? I think we do that in other commands.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's a good idea, i'll add as a follow up

Copy link
Copy Markdown
Contributor

@isaacroldan isaacroldan left a comment

Choose a reason for hiding this comment

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

Didn't tophat, for the command definition makes sense from dev-experience perspective 👌

@dengjeffrey dengjeffrey enabled auto-merge March 18, 2026 18:33
@github-actions
Copy link
Copy Markdown
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/private/node/session.d.ts
@@ -31,7 +31,7 @@ interface AppManagementAPIOauthOptions {
 /**
  * A scope supported by the Storefront Renderer API.
  */
-export type StorefrontRendererScope = 'devtools';
+export type StorefrontRendererScope = 'devtools' | 'graphql';
 interface StorefrontRendererAPIOAuthOptions {
     /** List of scopes to request permissions for. */
     scopes: StorefrontRendererScope[];

@dengjeffrey dengjeffrey added this pull request to the merge queue Mar 18, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Mar 18, 2026
@dengjeffrey dengjeffrey added this pull request to the merge queue Mar 18, 2026
Merged via the queue into main with commit 3dcf026 Mar 18, 2026
30 checks passed
@dengjeffrey dengjeffrey deleted the jd/quick-preview branch March 18, 2026 19:57
@dengjeffrey dengjeffrey mentioned this pull request Mar 18, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants