[Feature] Add Theme previews via JSON#6890
Conversation
Coverage report
Test suite run success3910 tests passing in 1502 suites. Report generated by 🧪jest coverage report action from 962e932 |
06454b5 to
f131575
Compare
| devFlags.path = resolvedSource | ||
| } else { | ||
| recordEvent('theme-command:dev:override-session') | ||
| await createOverrideDevSession(resolvedSource, devFlags, adminSession) |
There was a problem hiding this comment.
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.
This comment has been minimized.
This comment has been minimized.
karreiro
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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', | ||
| }), |
There was a problem hiding this comment.
While reviewing the flags for this command, I noticed we already have --path, so there are two options:
- When
--sourceis empty, document that the command uses the file system as the source and relies on--path. - Remove
--sourcein favor of--path, allowing you to pass either a--pathor an override file as a parameter.
I think option 2 is the cleaner approach. What do you think?
| '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', | ||
| }), |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Will do! Now that everything required to test this has been merged, I will give it a go tomorrow.
fedff6f to
d9a45af
Compare
| ...defaultHeaders(), | ||
| Authorization: `Bearer ${storefrontToken}`, | ||
| 'Content-Type': 'application/json', | ||
| }, |
There was a problem hiding this comment.
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.
|
🤖 Code Review · #projects-dev-ai for questions ✅ Complete - No issues 📋 History✅ 1 findings → ✅ 2 findings → ✅ No issues → ✅ No issues → ❌ Failed → ✅ No issues → ✅ 1 findings → ✅ No issues |
8d7b77c to
d94cecb
Compare
| recordEvent('theme-command:dev:override-session') | ||
| await createOverrideDevSession(devFlags.path, devFlags, adminSession) | ||
| return | ||
| } |
There was a problem hiding this comment.
--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, | ||
| }, |
There was a problem hiding this comment.
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).
| 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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
I'll move it out to an --overrides flag
65a7479 to
c9446c1
Compare
c9446c1 to
86501e1
Compare
karreiro
left a comment
There was a problem hiding this comment.
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):
|
@karreiro thanks for taking a look! Yea there were some changes to Identity. I'll DM you |
86501e1 to
e7a6bcb
Compare
| }: CreateThemePreviewOptions): Promise<ThemePreviewResult> { | ||
| recordTiming('theme-preview:create') | ||
| const baseUrl = buildBaseStorefrontUrl(session) | ||
| const url = `${baseUrl}/theme_preview.json?preview_theme_id=${themeId}` |
b686856 to
f2cf731
Compare
| '@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. |
There was a problem hiding this comment.
I think this changeset is outdated
9e0a893 to
f8dbc94
Compare
frandiox
left a comment
There was a problem hiding this comment.
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 diffcommand/utility to create it?
| const fileContent = await readFile(options.overrideJson) | ||
| let overrides: ThemeOverrides | ||
| try { | ||
| overrides = JSON.parse(fileContent) as ThemeOverrides |
There was a problem hiding this comment.
Should we also validate here the size limit and the expected JSON shape? Or are we going to rely solely on server validation?
There was a problem hiding this comment.
Solely rely on server validation so we don't repeat logic
| if (devFlags.overrides) { | ||
| recordEvent('theme-command:dev:override-session') | ||
| await createOverrideDevSession(devFlags.overrides, devFlags, adminSession) | ||
| return | ||
| } |
There was a problem hiding this comment.
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.
24b9e88 to
ce60476
Compare
frandiox
left a comment
There was a problem hiding this comment.
LGTM! Tested with normal auth and also with theme access app, it works 🎉 Thanks!
| renderSuccess({ | ||
| body: [ | ||
| { | ||
| list: { | ||
| title: options.previewIdentifier ? 'Preview updated' : 'Preview is ready', | ||
| items: [{link: {url: preview.url}}, `Preview ID: ${preview.preview_identifier}`], | ||
| }, | ||
| }, | ||
| ], | ||
| }) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
That's a good idea, i'll add as a follow up
isaacroldan
left a comment
There was a problem hiding this comment.
Didn't tophat, for the command definition makes sense from dev-experience perspective 👌
ce60476 to
962e932
Compare
Differences in type declarationsWe 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:
New type declarationsWe found no new type declarations in this PR Existing type declarationspackages/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[];
|
WHY are these changes introduced?
Resolves https://github.com/shop/issues-growth-activation/issues/1902
WHAT is this pull request doing?
theme previewto support temporarily overriding a theme via JSON and being able to preview itMore internal context: https://docs.google.com/document/d/13sa0iRs_DSgyPHbx9IIF64rvhh_KZCDLfJgwpSIxP4A/edit?usp=sharing
--overridespecifies the path to the override JSON to process--preview-idspecifies an existing preview session to be updated with the JSON value in sourceHow to test your changes?
https://docs.google.com/document/d/13sa0iRs_DSgyPHbx9IIF64rvhh_KZCDLfJgwpSIxP4A/edit?tab=t.0#bookmark=id.rhjcmeq18hn7
Post-release steps
https://github.com/shop/world/pull/483644
Measuring impact
How do we know this change was effective? Please choose one:
Checklist