diff --git a/.changeset/slick-humans-enjoy.md b/.changeset/slick-humans-enjoy.md new file mode 100644 index 0000000000..e0a35dada2 --- /dev/null +++ b/.changeset/slick-humans-enjoy.md @@ -0,0 +1,6 @@ +--- +'@shopify/theme': minor +'@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. diff --git a/docs-shopify.dev/commands/interfaces/theme-dev.interface.ts b/docs-shopify.dev/commands/interfaces/theme-dev.interface.ts index 311f0b9d57..172124f95e 100644 --- a/docs-shopify.dev/commands/interfaces/theme-dev.interface.ts +++ b/docs-shopify.dev/commands/interfaces/theme-dev.interface.ts @@ -78,6 +78,12 @@ export interface themedev { */ '--open'?: '' + /** + * Path to a JSON overrides file. When provided, overrides are applied to the theme specified by --theme instead of uploading a local theme. + * @environment SHOPIFY_FLAG_OVERRIDES + */ + '--overrides '?: string + /** * Password generated from the Theme Access app or an Admin API token. * @environment SHOPIFY_CLI_THEME_TOKEN @@ -96,6 +102,12 @@ export interface themedev { */ '--port '?: string + /** + * An existing preview identifier to update instead of creating a new preview. Used with --overrides. + * @environment SHOPIFY_FLAG_PREVIEW_ID + */ + '--preview-id '?: string + /** * Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com). * @environment SHOPIFY_FLAG_STORE diff --git a/docs-shopify.dev/commands/theme-dev.doc.ts b/docs-shopify.dev/commands/theme-dev.doc.ts index 3f6a1c3029..3ea04b483d 100644 --- a/docs-shopify.dev/commands/theme-dev.doc.ts +++ b/docs-shopify.dev/commands/theme-dev.doc.ts @@ -6,7 +6,9 @@ const data: ReferenceEntityTemplateSchema = { description: ` Uploads the current theme as the specified theme, or a [development theme](/docs/themes/tools/cli#development-themes), to a store so you can preview it. -This command returns the following information: + Alternatively, a JSON overrides file can be specified using --overrides to quickly preview changes without uploading a theme. + +This command returns the following information by default, unless --overrides is used to target a JSON overrides file: - A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data. @@ -16,14 +18,16 @@ This command returns the following information: - A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers. +> Note: When using --overrides to target a JSON overrides file, the command will return the preview link instead of the development theme URL. + If you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the \`--theme-editor-sync\` flag. > Note: You can't preview checkout customizations using http://127.0.0.1:9292. Development themes are deleted when you run \`shopify auth logout\`. If you need a preview link that can be used after you log out, then you should [share](/docs/api/shopify-cli/theme/theme-share) your theme or [push](/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store. -You can run this command only in a directory that matches the [default Shopify theme folder structure](/docs/themes/tools/cli#directory-structure).`, - overviewPreviewDescription: `Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time.`, +You can run this command only in a directory that matches the [default Shopify theme folder structure](/docs/themes/tools/cli#directory-structure) unless --overrides is used to target a JSON overrides file.`, + overviewPreviewDescription: `Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time. Alternatively, a JSON overrides file can be provided via --overrides to quickly preview changes without uploading a theme.`, type: 'command', isVisualComponent: false, defaultExample: { diff --git a/docs-shopify.dev/generated/generated_docs_data.json b/docs-shopify.dev/generated/generated_docs_data.json index d25a20bd95..79f0e3a4ba 100644 --- a/docs-shopify.dev/generated/generated_docs_data.json +++ b/docs-shopify.dev/generated/generated_docs_data.json @@ -5994,8 +5994,8 @@ }, { "name": "theme dev", - "description": "\n Uploads the current theme as the specified theme, or a [development theme](/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\nThis command returns the following information:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the [editor](/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should [share](/docs/api/shopify-cli/theme/theme-share) your theme or [push](/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the [default Shopify theme folder structure](/docs/themes/tools/cli#directory-structure).", - "overviewPreviewDescription": "Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time.", + "description": "\n Uploads the current theme as the specified theme, or a [development theme](/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\n Alternatively, a JSON overrides file can be specified using --overrides to quickly preview changes without uploading a theme.\n\nThis command returns the following information by default, unless --overrides is used to target a JSON overrides file:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the [editor](/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\n> Note: When using --overrides to target a JSON overrides file, the command will return the preview link instead of the development theme URL.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should [share](/docs/api/shopify-cli/theme/theme-share) your theme or [push](/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the [default Shopify theme folder structure](/docs/themes/tools/cli#directory-structure) unless --overrides is used to target a JSON overrides file.", + "overviewPreviewDescription": "Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time. Alternatively, a JSON overrides file can be provided via --overrides to quickly preview changes without uploading a theme.", "type": "command", "isVisualComponent": false, "defaultExample": { @@ -6084,6 +6084,15 @@ "isOptional": true, "environmentValue": "SHOPIFY_FLAG_OPEN" }, + { + "filePath": "docs-shopify.dev/commands/interfaces/theme-dev.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--overrides ", + "value": "string", + "description": "Path to a JSON overrides file. When provided, overrides are applied to the theme specified by --theme instead of uploading a local theme.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_OVERRIDES" + }, { "filePath": "docs-shopify.dev/commands/interfaces/theme-dev.interface.ts", "syntaxKind": "PropertySignature", @@ -6111,6 +6120,15 @@ "isOptional": true, "environmentValue": "SHOPIFY_FLAG_PORT" }, + { + "filePath": "docs-shopify.dev/commands/interfaces/theme-dev.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--preview-id ", + "value": "string", + "description": "An existing preview identifier to update instead of creating a new preview. Used with --overrides.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_PREVIEW_ID" + }, { "filePath": "docs-shopify.dev/commands/interfaces/theme-dev.interface.ts", "syntaxKind": "PropertySignature", @@ -6202,7 +6220,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 '?: 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 '?: 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 '?: string\n\n /**\n * Skip hot reloading any files that match the specified pattern.\n * @environment SHOPIFY_FLAG_IGNORE\n */\n '-x, --ignore '?: 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 '?: 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 '?: 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 '?: string\n\n /**\n * Hot reload only files that match the specified pattern.\n * @environment SHOPIFY_FLAG_ONLY\n */\n '-o, --only '?: 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 '?: 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 '?: string\n\n /**\n * Local port to serve theme preview from.\n * @environment SHOPIFY_FLAG_PORT\n */\n '--port '?: string\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 '?: string\n\n /**\n * The password for storefronts with password protection.\n * @environment SHOPIFY_FLAG_STORE_PASSWORD\n */\n '--store-password '?: string\n\n /**\n * Theme ID or name of the remote theme.\n * @environment SHOPIFY_FLAG_THEME_ID\n */\n '-t, --theme '?: 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 '?: 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 '?: 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 '?: string\n\n /**\n * Skip hot reloading any files that match the specified pattern.\n * @environment SHOPIFY_FLAG_IGNORE\n */\n '-x, --ignore '?: 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 '?: 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 '?: 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 '?: string\n\n /**\n * Hot reload only files that match the specified pattern.\n * @environment SHOPIFY_FLAG_ONLY\n */\n '-o, --only '?: 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 * Path to a JSON overrides file. When provided, overrides are applied to the theme specified by --theme instead of uploading a local theme.\n * @environment SHOPIFY_FLAG_OVERRIDES\n */\n '--overrides '?: string\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password '?: 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 '?: string\n\n /**\n * Local port to serve theme preview from.\n * @environment SHOPIFY_FLAG_PORT\n */\n '--port '?: string\n\n /**\n * An existing preview identifier to update instead of creating a new preview. Used with --overrides.\n * @environment SHOPIFY_FLAG_PREVIEW_ID\n */\n '--preview-id '?: string\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 '?: string\n\n /**\n * The password for storefronts with password protection.\n * @environment SHOPIFY_FLAG_STORE_PASSWORD\n */\n '--store-password '?: string\n\n /**\n * Theme ID or name of the remote theme.\n * @environment SHOPIFY_FLAG_THEME_ID\n */\n '-t, --theme '?: 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}" } } } diff --git a/packages/cli-kit/src/private/node/session.ts b/packages/cli-kit/src/private/node/session.ts index 46d48594c4..6f87d00c66 100644 --- a/packages/cli-kit/src/private/node/session.ts +++ b/packages/cli-kit/src/private/node/session.ts @@ -80,7 +80,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[] diff --git a/packages/cli-kit/src/private/node/session/scopes.ts b/packages/cli-kit/src/private/node/session/scopes.ts index bced523106..475ea4e578 100644 --- a/packages/cli-kit/src/private/node/session/scopes.ts +++ b/packages/cli-kit/src/private/node/session/scopes.ts @@ -48,7 +48,7 @@ function defaultApiScopes(api: API): string[] { case 'admin': return ['graphql', 'themes', 'collaborator'] case 'storefront-renderer': - return ['devtools'] + return ['devtools', 'graphql'] case 'partners': return ['cli'] case 'business-platform': diff --git a/packages/cli/README.md b/packages/cli/README.md index 63fa029953..9a29f62056 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -2110,14 +2110,14 @@ DESCRIPTION ## `shopify theme dev` -Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time. +Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time. Alternatively, a JSON overrides file can be provided via --overrides to quickly preview changes without uploading a theme. ``` USAGE $ shopify theme dev [-a] [-e ...] [--error-overlay silent|default] [--host ] [-x ...] [--listing ] [--live-reload hot-reload|full-page|off] [--no-color] [-n] [--notify ] [-o ...] - [--open] [--password ] [--path ] [--port ] [-s ] [--store-password ] [-t ] - [--theme-editor-sync] [--verbose] + [--open] [--overrides ] [--password ] [--path ] [--port ] [--preview-id ] [-s + ] [--store-password ] [-t ] [--theme-editor-sync] [--verbose] FLAGS -a, --allow-live @@ -2177,6 +2177,10 @@ FLAGS --open [env: SHOPIFY_FLAG_OPEN] Automatically launch the theme preview in your default web browser. + --overrides= + [env: SHOPIFY_FLAG_OVERRIDES] Path to a JSON overrides file. When provided, overrides are applied to the theme + specified by --theme instead of uploading a local theme. + --password= [env: SHOPIFY_CLI_THEME_TOKEN] Password generated from the Theme Access app or an Admin API token. @@ -2186,6 +2190,10 @@ FLAGS --port= [env: SHOPIFY_FLAG_PORT] Local port to serve theme preview from. + --preview-id= + [env: SHOPIFY_FLAG_PREVIEW_ID] An existing preview identifier to update instead of creating a new preview. Used with + --overrides. + --store-password= [env: SHOPIFY_FLAG_STORE_PASSWORD] The password for storefronts with password protection. @@ -2197,13 +2205,17 @@ FLAGS DESCRIPTION Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to - your terminal. While running, changes will push to the store in real time. + your terminal. While running, changes will push to the store in real time. Alternatively, a JSON overrides file can be + provided via --overrides to quickly preview changes without uploading a theme. Uploads the current theme as the specified theme, or a "development theme" (https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it. - This command returns the following information: + Alternatively, a JSON overrides file can be specified using --overrides to quickly preview changes without uploading a + theme. + + This command returns the following information by default, unless --overrides is used to target a JSON overrides file: - A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the @@ -2217,6 +2229,9 @@ DESCRIPTION (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers. + > Note: When using --overrides to target a JSON overrides file, the command will return the preview link instead of + the development theme URL. + If you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag. @@ -2227,7 +2242,8 @@ DESCRIPTION (https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store. You can run this command only in a directory that matches the "default Shopify theme folder structure" - (https://shopify.dev/docs/themes/tools/cli#directory-structure). + (https://shopify.dev/docs/themes/tools/cli#directory-structure) unless --overrides is used to target a JSON overrides + file. ``` ## `shopify theme duplicate` diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index ba48270931..316e5386f5 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -5948,8 +5948,8 @@ "args": { }, "customPluginName": "@shopify/theme", - "description": "\n Uploads the current theme as the specified theme, or a \"development theme\" (https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\nThis command returns the following information:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should \"share\" (https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or \"push\" (https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the \"default Shopify theme folder structure\" (https://shopify.dev/docs/themes/tools/cli#directory-structure).", - "descriptionWithMarkdown": "\n Uploads the current theme as the specified theme, or a [development theme](https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\nThis command returns the following information:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should [share](https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or [push](https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure).", + "description": "\n Uploads the current theme as the specified theme, or a \"development theme\" (https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\n Alternatively, a JSON overrides file can be specified using --overrides to quickly preview changes without uploading a theme.\n\nThis command returns the following information by default, unless --overrides is used to target a JSON overrides file:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\n> Note: When using --overrides to target a JSON overrides file, the command will return the preview link instead of the development theme URL.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should \"share\" (https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or \"push\" (https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the \"default Shopify theme folder structure\" (https://shopify.dev/docs/themes/tools/cli#directory-structure) unless --overrides is used to target a JSON overrides file.", + "descriptionWithMarkdown": "\n Uploads the current theme as the specified theme, or a [development theme](https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\n Alternatively, a JSON overrides file can be specified using --overrides to quickly preview changes without uploading a theme.\n\nThis command returns the following information by default, unless --overrides is used to target a JSON overrides file:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\n> Note: When using --overrides to target a JSON overrides file, the command will return the preview link instead of the development theme URL.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should [share](https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or [push](https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure) unless --overrides is used to target a JSON overrides file.", "flags": { "allow-live": { "allowNo": false, @@ -6069,6 +6069,14 @@ "name": "open", "type": "boolean" }, + "overrides": { + "description": "Path to a JSON overrides file. When provided, overrides are applied to the theme specified by --theme instead of uploading a local theme.", + "env": "SHOPIFY_FLAG_OVERRIDES", + "hasDynamicHelp": false, + "multiple": false, + "name": "overrides", + "type": "option" + }, "password": { "description": "Password generated from the Theme Access app or an Admin API token.", "env": "SHOPIFY_CLI_THEME_TOKEN", @@ -6102,6 +6110,14 @@ "name": "port", "type": "option" }, + "preview-id": { + "description": "An existing preview identifier to update instead of creating a new preview. Used with --overrides.", + "env": "SHOPIFY_FLAG_PREVIEW_ID", + "hasDynamicHelp": false, + "multiple": false, + "name": "preview-id", + "type": "option" + }, "store": { "char": "s", "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", @@ -6153,7 +6169,7 @@ "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time." + "summary": "Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time. Alternatively, a JSON overrides file can be provided via --overrides to quickly preview changes without uploading a theme." }, "theme:duplicate": { "aliases": [ @@ -7465,8 +7481,8 @@ "args": { }, "customPluginName": "@shopify/theme", - "description": "\n Uploads the current theme as the specified theme, or a \"development theme\" (https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\nThis command returns the following information:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should \"share\" (https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or \"push\" (https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the \"default Shopify theme folder structure\" (https://shopify.dev/docs/themes/tools/cli#directory-structure).", - "descriptionWithMarkdown": "\n Uploads the current theme as the specified theme, or a [development theme](https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\nThis command returns the following information:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should [share](https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or [push](https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure).", + "description": "\n Uploads the current theme as the specified theme, or a \"development theme\" (https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\n Alternatively, a JSON overrides file can be specified using --overrides to quickly preview changes without uploading a theme.\n\nThis command returns the following information by default, unless --overrides is used to target a JSON overrides file:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\n> Note: When using --overrides to target a JSON overrides file, the command will return the preview link instead of the development theme URL.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should \"share\" (https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or \"push\" (https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the \"default Shopify theme folder structure\" (https://shopify.dev/docs/themes/tools/cli#directory-structure) unless --overrides is used to target a JSON overrides file.", + "descriptionWithMarkdown": "\n Uploads the current theme as the specified theme, or a [development theme](https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\n Alternatively, a JSON overrides file can be specified using --overrides to quickly preview changes without uploading a theme.\n\nThis command returns the following information by default, unless --overrides is used to target a JSON overrides file:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\n> Note: When using --overrides to target a JSON overrides file, the command will return the preview link instead of the development theme URL.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should [share](https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or [push](https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure) unless --overrides is used to target a JSON overrides file.", "flags": { "allow-live": { "allowNo": false, @@ -7586,6 +7602,14 @@ "name": "open", "type": "boolean" }, + "overrides": { + "description": "Path to a JSON overrides file. When provided, overrides are applied to the theme specified by --theme instead of uploading a local theme.", + "env": "SHOPIFY_FLAG_OVERRIDES", + "hasDynamicHelp": false, + "multiple": false, + "name": "overrides", + "type": "option" + }, "password": { "description": "Password generated from the Theme Access app or an Admin API token.", "env": "SHOPIFY_CLI_THEME_TOKEN", @@ -7619,6 +7643,14 @@ "name": "port", "type": "option" }, + "preview-id": { + "description": "An existing preview identifier to update instead of creating a new preview. Used with --overrides.", + "env": "SHOPIFY_FLAG_PREVIEW_ID", + "hasDynamicHelp": false, + "multiple": false, + "name": "preview-id", + "type": "option" + }, "store": { "char": "s", "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", @@ -7670,7 +7702,7 @@ "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "summary": "Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time." + "summary": "Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time. Alternatively, a JSON overrides file can be provided via --overrides to quickly preview changes without uploading a theme." }, "theme:share": { "aliases": [ diff --git a/packages/theme/src/cli/commands/theme/dev.test.ts b/packages/theme/src/cli/commands/theme/dev.test.ts new file mode 100644 index 0000000000..409de61010 --- /dev/null +++ b/packages/theme/src/cli/commands/theme/dev.test.ts @@ -0,0 +1,175 @@ +import Dev from './dev.js' +import {dev} from '../../services/dev.js' +import {devWithOverrideFile} from '../../services/dev-override.js' +import {DevelopmentThemeManager} from '../../utilities/development-theme-manager.js' +import {findOrSelectTheme} from '../../utilities/theme-selector.js' +import {ensureLiveThemeConfirmed} from '../../utilities/theme-ui.js' +import {metafieldsPull} from '../../services/metafields-pull.js' +import {ensureThemeStore} from '../../utilities/theme-store.js' +import {fileExistsSync} from '@shopify/cli-kit/node/fs' +import {DEVELOPMENT_THEME_ROLE} from '@shopify/cli-kit/node/themes/utils' +import {buildTheme} from '@shopify/cli-kit/node/themes/factories' +import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' +import {Config} from '@oclif/core' +import {describe, vi, expect, test, beforeEach} from 'vitest' + +vi.mock('@shopify/cli-kit/node/session') +vi.mock('@shopify/cli-kit/node/fs') +vi.mock('@shopify/cli-kit/node/analytics', () => ({ + recordEvent: vi.fn(), + compileData: vi.fn().mockReturnValue({timings: {}, errors: {}, retries: {}, events: {}}), +})) +vi.mock('@shopify/cli-kit/node/metadata', () => ({ + addPublicMetadata: vi.fn(), + addSensitiveMetadata: vi.fn(), +})) +vi.mock('@shopify/cli-kit/node/environments') +vi.mock('../../services/dev.js') +vi.mock('../../services/dev-override.js') +vi.mock('../../utilities/development-theme-manager.js') +vi.mock('../../utilities/theme-selector.js') +vi.mock('../../utilities/theme-ui.js') +vi.mock('../../services/metafields-pull.js') +vi.mock('../../utilities/theme-store.js') + +const CommandConfig = new Config({root: __dirname}) + +const adminSession = {token: 'test-token', storeFqdn: 'test-store.myshopify.com'} +const devTheme = buildTheme({id: 1, name: 'Dev Theme', role: DEVELOPMENT_THEME_ROLE})! +const namedTheme = buildTheme({id: 2, name: 'My Theme', role: 'unpublished'})! + +async function run(argv: string[]) { + await CommandConfig.load() + const command = new Dev(['--store=test-store.myshopify.com', '--path=/theme', ...argv], CommandConfig) + await command.run() +} + +describe('Dev', () => { + beforeEach(() => { + vi.mocked(ensureThemeStore).mockReturnValue('test-store.myshopify.com') + vi.mocked(ensureAuthenticatedThemes).mockResolvedValue(adminSession) + vi.mocked(fileExistsSync).mockReturnValue(true) + vi.mocked(ensureLiveThemeConfirmed).mockResolvedValue(true) + vi.mocked(metafieldsPull).mockResolvedValue(undefined) + vi.mocked(dev).mockResolvedValue(undefined) + vi.mocked(DevelopmentThemeManager).mockImplementation( + () => + ({ + findOrCreate: vi.fn().mockResolvedValue({...devTheme, createdAtRuntime: false}), + } as any), + ) + }) + + describe('--overrides flag', () => { + test('calls devWithOverrideFile when --overrides and --theme are provided', async () => { + // Given + vi.mocked(findOrSelectTheme).mockResolvedValue(namedTheme) + vi.mocked(devWithOverrideFile).mockResolvedValue(undefined) + + // When + await run(['--overrides=/path/to/overrides.json', '--theme=My Theme']) + + // Then + expect(devWithOverrideFile).toHaveBeenCalledWith( + expect.objectContaining({ + adminSession, + overrideJson: '/path/to/overrides.json', + themeId: namedTheme.id.toString(), + open: false, + }), + ) + expect(dev).not.toHaveBeenCalled() + }) + + test('passes --preview-id to devWithOverrideFile when provided', async () => { + // Given + vi.mocked(findOrSelectTheme).mockResolvedValue(namedTheme) + vi.mocked(devWithOverrideFile).mockResolvedValue(undefined) + + // When + await run(['--overrides=/path/to/overrides.json', '--theme=My Theme', '--preview-id=abc123']) + + // Then + expect(devWithOverrideFile).toHaveBeenCalledWith( + expect.objectContaining({ + previewIdentifier: 'abc123', + }), + ) + }) + + test('throws AbortError when --overrides is used without --theme', async () => { + // When/Then + await expect(run(['--overrides=/path/to/overrides.json'])).rejects.toThrow( + 'The --theme flag is required when using --overrides.', + ) + expect(devWithOverrideFile).not.toHaveBeenCalled() + }) + + test('does not run normal dev flow when --overrides is provided', async () => { + // Given + vi.mocked(findOrSelectTheme).mockResolvedValue(namedTheme) + vi.mocked(devWithOverrideFile).mockResolvedValue(undefined) + + // When + await run(['--overrides=/path/to/overrides.json', '--theme=My Theme']) + + // Then + expect(dev).not.toHaveBeenCalled() + }) + }) + + describe('normal dev flow', () => { + test('creates a development theme when no --theme flag is provided', async () => { + // When + await run([]) + + // Then + expect(DevelopmentThemeManager).toHaveBeenCalledWith(adminSession) + expect(dev).toHaveBeenCalledWith( + expect.objectContaining({ + directory: '/theme', + theme: devTheme, + }), + ) + }) + + test('finds the specified theme when --theme flag is provided', async () => { + // Given + vi.mocked(findOrSelectTheme).mockResolvedValue(namedTheme) + + // When + await run(['--theme=My Theme']) + + // Then + expect(findOrSelectTheme).toHaveBeenCalledWith(adminSession, {filter: {theme: 'My Theme'}}) + expect(dev).toHaveBeenCalledWith( + expect.objectContaining({ + theme: namedTheme, + }), + ) + }) + + test('runs metafieldsPull after dev', async () => { + // When + await run([]) + + // Then + expect(metafieldsPull).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/theme', + }), + ) + }) + + test('returns early when live theme is not confirmed', async () => { + // Given + vi.mocked(ensureLiveThemeConfirmed).mockResolvedValue(false) + + // When + await run([]) + + // Then + expect(dev).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/theme/src/cli/commands/theme/dev.ts b/packages/theme/src/cli/commands/theme/dev.ts index 99eb6c25b7..a16edce67b 100644 --- a/packages/theme/src/cli/commands/theme/dev.ts +++ b/packages/theme/src/cli/commands/theme/dev.ts @@ -1,6 +1,7 @@ import {themeFlags} from '../../flags.js' import ThemeCommand, {RequiredFlags} from '../../utilities/theme-command.js' import {dev} from '../../services/dev.js' +import {devWithOverrideFile} from '../../services/dev-override.js' import {DevelopmentThemeManager} from '../../utilities/development-theme-manager.js' import {findOrSelectTheme} from '../../utilities/theme-selector.js' import {metafieldsPull} from '../../services/metafields-pull.js' @@ -10,6 +11,7 @@ import {globalFlags} from '@shopify/cli-kit/node/cli' import {Theme} from '@shopify/cli-kit/node/themes/types' import {recordEvent} from '@shopify/cli-kit/node/analytics' import {AdminSession} from '@shopify/cli-kit/node/session' +import {AbortError} from '@shopify/cli-kit/node/error' import {InferredFlags} from '@oclif/core/interfaces' import type {ErrorOverlayMode, LiveReload} from '../../utilities/theme-environment/types.js' @@ -18,12 +20,14 @@ type DevFlags = InferredFlags export default class Dev extends ThemeCommand { static summary = - 'Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time.' + 'Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time. Alternatively, a JSON overrides file can be provided via --overrides to quickly preview changes without uploading a theme.' static descriptionWithMarkdown = ` Uploads the current theme as the specified theme, or a [development theme](https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it. -This command returns the following information: + Alternatively, a JSON overrides file can be specified using --overrides to quickly preview changes without uploading a theme. + +This command returns the following information by default, unless --overrides is used to target a JSON overrides file: - A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data. @@ -33,13 +37,15 @@ This command returns the following information: - A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers. +> Note: When using --overrides to target a JSON overrides file, the command will return the preview link instead of the development theme URL. + If you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the \`--theme-editor-sync\` flag. > Note: You can't preview checkout customizations using http://127.0.0.1:9292. Development themes are deleted when you run \`shopify auth logout\`. If you need a preview link that can be used after you log out, then you should [share](https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or [push](https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store. -You can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure).` +You can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure) unless --overrides is used to target a JSON overrides file.` static description = this.descriptionWithoutMarkdown() @@ -135,6 +141,15 @@ You can run this command only in a directory that matches the [default Shopify t env: 'SHOPIFY_FLAG_ALLOW_LIVE', default: false, }), + overrides: Flags.string({ + description: + 'Path to a JSON overrides file. When provided, overrides are applied to the theme specified by --theme instead of uploading a local theme.', + env: 'SHOPIFY_FLAG_OVERRIDES', + }), + 'preview-id': Flags.string({ + description: 'An existing preview identifier to update instead of creating a new preview. Used with --overrides.', + env: 'SHOPIFY_FLAG_PREVIEW_ID', + }), } static multiEnvironmentsFlags: RequiredFlags = null @@ -144,6 +159,12 @@ You can run this command only in a directory that matches the [default Shopify t recordEvent('theme-command:dev:single-env:authenticated') + if (devFlags.overrides) { + recordEvent('theme-command:dev:override-session') + await createOverrideDevSession(devFlags.overrides, devFlags, adminSession) + return + } + let theme: Theme let flags @@ -196,3 +217,18 @@ You can run this command only in a directory that matches the [default Shopify t }) } } + +async function createOverrideDevSession(overrideJson: string, devFlags: DevFlags, adminSession: AdminSession) { + if (!devFlags.theme) { + throw new AbortError(`The --theme flag is required when using --overrides.`) + } + + const theme = await findOrSelectTheme(adminSession, {filter: {theme: devFlags.theme}}) + await devWithOverrideFile({ + adminSession, + overrideJson, + themeId: theme.id.toString(), + previewIdentifier: devFlags['preview-id'], + open: devFlags.open, + }) +} diff --git a/packages/theme/src/cli/services/dev-override.test.ts b/packages/theme/src/cli/services/dev-override.test.ts new file mode 100644 index 0000000000..80c6234f66 --- /dev/null +++ b/packages/theme/src/cli/services/dev-override.test.ts @@ -0,0 +1,172 @@ +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 {describe, expect, test, vi} from 'vitest' +import {renderSuccess} from '@shopify/cli-kit/node/ui' +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('./dev.js', () => ({openURLSafely: vi.fn()})) +vi.mock('@shopify/cli-kit/node/ui') +vi.mock('@shopify/cli-kit/node/fs') + +const adminSession = {token: 'token', storeFqdn: 'store.myshopify.com'} +const mockSession = { + token: 'token', + storeFqdn: 'store.myshopify.com', + storefrontToken: 'sf_token', + sessionCookies: {}, +} +const expectedPreviewUrl = 'https://abc123.shopifypreview.com' +const expectedPreviewId = 'abc123' + +describe('devWithOverrideFile', () => { + test('throws when override file does not exist', async () => { + // Given + vi.mocked(fileExistsSync).mockReturnValue(false) + + // When/Then + await expect( + devWithOverrideFile({adminSession, overrideJson: '/missing.json', themeId: '123', open: false}), + ).rejects.toThrow('Override file not found: /missing.json') + }) + + test('creates a preview when no previewIdentifier is provided', async () => { + // Given + vi.mocked(fileExistsSync).mockReturnValue(true) + vi.mocked(readFile).mockResolvedValue(Buffer.from(JSON.stringify({templates: {}}))) + vi.mocked(fetchDevServerSession).mockResolvedValue(mockSession) + vi.mocked(createThemePreview).mockResolvedValue({url: expectedPreviewUrl, preview_identifier: expectedPreviewId}) + + // When + await devWithOverrideFile({adminSession, overrideJson: '/overrides.json', themeId: '789', open: false}) + + // Then + expect(createThemePreview).toHaveBeenCalledWith( + expect.objectContaining({ + session: mockSession, + storefrontToken: 'sf_token', + }), + ) + expect(updateThemePreview).not.toHaveBeenCalled() + expect(renderSuccess).toHaveBeenCalledWith( + expect.objectContaining({ + body: [ + { + list: { + title: 'Preview is ready', + items: [{link: {url: expectedPreviewUrl}}, `Preview ID: ${expectedPreviewId}`], + }, + }, + ], + }), + ) + }) + + test('updates a preview when previewIdentifier is provided', async () => { + // Given + vi.mocked(fileExistsSync).mockReturnValue(true) + vi.mocked(readFile).mockResolvedValue(Buffer.from(JSON.stringify({templates: {}}))) + vi.mocked(fetchDevServerSession).mockResolvedValue(mockSession) + vi.mocked(updateThemePreview).mockResolvedValue({url: expectedPreviewUrl, preview_identifier: expectedPreviewId}) + + // When + await devWithOverrideFile({ + adminSession, + overrideJson: '/overrides.json', + themeId: '789', + previewIdentifier: expectedPreviewId, + open: false, + }) + + // Then + expect(updateThemePreview).toHaveBeenCalledWith( + expect.objectContaining({ + session: mockSession, + storefrontToken: 'sf_token', + previewIdentifier: expectedPreviewId, + }), + ) + expect(createThemePreview).not.toHaveBeenCalled() + expect(renderSuccess).toHaveBeenCalledWith( + expect.objectContaining({ + body: [ + { + list: { + title: 'Preview updated', + items: [{link: {url: expectedPreviewUrl}}, `Preview ID: ${expectedPreviewId}`], + }, + }, + ], + }), + ) + }) + + test('injects theme_id into overrides metadata', async () => { + // Given + vi.mocked(fileExistsSync).mockReturnValue(true) + vi.mocked(readFile).mockResolvedValue(Buffer.from(JSON.stringify({templates: {}, metadata: {version: '1.0.0'}}))) + vi.mocked(fetchDevServerSession).mockResolvedValue(mockSession) + vi.mocked(createThemePreview).mockResolvedValue({url: expectedPreviewUrl, preview_identifier: expectedPreviewId}) + const expectedMetadata = {version: '1.0.0', theme_id: '1.0'} + + // When + await devWithOverrideFile({ + adminSession, + overrideJson: '/overrides.json', + themeId: expectedMetadata.theme_id, + open: false, + }) + + // Then + const passedContent = vi.mocked(createThemePreview).mock.calls[0]![0].overridesContent + const parsed = JSON.parse(passedContent) + expect(parsed.metadata).toEqual(expect.objectContaining(expectedMetadata)) + }) + + test('throws when override file contains invalid JSON', async () => { + // Given + vi.mocked(fileExistsSync).mockReturnValue(true) + vi.mocked(readFile).mockResolvedValue(Buffer.from('not valid json')) + + // When/Then + const error = await devWithOverrideFile({ + adminSession, + overrideJson: '/bad.json', + themeId: '123', + open: false, + }).catch((err) => err) + expect(error.message).toBe('Failed to parse override file: /bad.json') + expect(error.tryMessage).toMatch(/not valid json/i) + }) + + test('opens the preview URL when open is true', async () => { + // Given + vi.mocked(fileExistsSync).mockReturnValue(true) + vi.mocked(readFile).mockResolvedValue(Buffer.from(JSON.stringify({templates: {}}))) + vi.mocked(fetchDevServerSession).mockResolvedValue(mockSession) + vi.mocked(createThemePreview).mockResolvedValue({url: expectedPreviewUrl, preview_identifier: expectedPreviewId}) + + // When + await devWithOverrideFile({adminSession, overrideJson: '/overrides.json', themeId: '789', open: true}) + + // Then + expect(openURLSafely).toHaveBeenCalledWith(expectedPreviewUrl, 'theme preview') + }) + + test('does not open the preview URL when open is false', async () => { + // Given + vi.mocked(fileExistsSync).mockReturnValue(true) + vi.mocked(readFile).mockResolvedValue(Buffer.from(JSON.stringify({templates: {}}))) + vi.mocked(fetchDevServerSession).mockResolvedValue(mockSession) + vi.mocked(createThemePreview).mockResolvedValue({url: expectedPreviewUrl, preview_identifier: expectedPreviewId}) + + // When + await devWithOverrideFile({adminSession, overrideJson: '/overrides.json', themeId: '789', open: false}) + + // Then + expect(openURLSafely).not.toHaveBeenCalled() + }) +}) diff --git a/packages/theme/src/cli/services/dev-override.ts b/packages/theme/src/cli/services/dev-override.ts new file mode 100644 index 0000000000..9f06a2008c --- /dev/null +++ b/packages/theme/src/cli/services/dev-override.ts @@ -0,0 +1,81 @@ +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 {renderSuccess} from '@shopify/cli-kit/node/ui' +import {AdminSession} from '@shopify/cli-kit/node/session' +import {AbortError} from '@shopify/cli-kit/node/error' +import {readFile, fileExistsSync} from '@shopify/cli-kit/node/fs' + +interface ThemeOverrides { + [key: string]: unknown +} + +interface DevWithOverrideFileOptions { + adminSession: AdminSession + overrideJson: string + themeId: string + previewIdentifier?: string + open: boolean +} + +/** + * Reads a JSON overrides file and creates or updates a Storefront preview. + * The resulting preview URL is displayed to the user. + */ +export async function devWithOverrideFile(options: DevWithOverrideFileOptions) { + if (!fileExistsSync(options.overrideJson)) { + throw new AbortError(`Override file not found: ${options.overrideJson}`) + } + + const fileContent = await readFile(options.overrideJson) + let overrides: ThemeOverrides + try { + overrides = JSON.parse(fileContent) as ThemeOverrides + } catch (error) { + const reason = error instanceof Error ? error.message : String(error) + throw new AbortError(`Failed to parse override file: ${options.overrideJson}`, reason) + } + + const session = await fetchDevServerSession(options.themeId, options.adminSession) + const overridesContent = constructOverrides(overrides, options.themeId) + + const preview = options.previewIdentifier + ? await updateThemePreview({ + session, + storefrontToken: session.storefrontToken, + overridesContent, + previewIdentifier: options.previewIdentifier, + }) + : await createThemePreview({ + session, + storefrontToken: session.storefrontToken, + overridesContent, + }) + + renderSuccess({ + body: [ + { + list: { + title: options.previewIdentifier ? 'Preview updated' : 'Preview is ready', + items: [{link: {url: preview.url}}, `Preview ID: ${preview.preview_identifier}`], + }, + }, + ], + }) + + if (options.open) { + openURLSafely(preview.url, 'theme preview') + } +} + +function constructOverrides(overrides: ThemeOverrides, themeId: string): string { + const existingMetadata = (overrides.metadata ?? {}) as ThemeOverrides + + return JSON.stringify({ + ...overrides, + metadata: { + ...existingMetadata, + theme_id: themeId, + }, + }) +} diff --git a/packages/theme/src/cli/utilities/theme-previews/preview.test.ts b/packages/theme/src/cli/utilities/theme-previews/preview.test.ts new file mode 100644 index 0000000000..5bfdb43f21 --- /dev/null +++ b/packages/theme/src/cli/utilities/theme-previews/preview.test.ts @@ -0,0 +1,165 @@ +import {createThemePreview, updateThemePreview} from './preview.js' +import {DevServerSession} from '../theme-environment/types.js' +import {describe, expect, test, vi} from 'vitest' +import {shopifyFetch, Response} from '@shopify/cli-kit/node/http' + +vi.mock('@shopify/cli-kit/node/http') + +const session: DevServerSession = { + token: 'admin_token_abc123', + storeFqdn: 'store.myshopify.com', + storefrontToken: 'token_111222333', + storefrontPassword: 'password', + sessionCookies: {}, +} + +function jsonResponse(body: object, options: {status?: number; statusText?: string} = {}): Response { + const {status = 200, statusText = 'OK'} = options + return { + ok: status >= 200 && status < 300, + status, + statusText, + json: () => Promise.resolve(body), + } as unknown as Response +} + +describe('createThemePreview', () => { + test('POSTs to the preview endpoint and returns url and preview_identifier', async () => { + // Given + const overrides = JSON.stringify({templates: {'index.liquid': '

Hello

'}}) + const responseBody = {url: 'https://abc.shopifypreview.com', preview_identifier: 'abc'} + vi.mocked(shopifyFetch).mockResolvedValue(jsonResponse(responseBody)) + + // When + const result = await createThemePreview({ + session, + storefrontToken: 'sf_token', + overridesContent: overrides, + }) + + // Then + expect(result).toEqual(responseBody) + expect(shopifyFetch).toHaveBeenCalledWith( + `https://${session.storeFqdn}/theme_preview.json`, + expect.objectContaining({ + method: 'POST', + body: overrides, + headers: expect.objectContaining({ + Authorization: 'Bearer sf_token', + 'Content-Type': 'application/json', + }), + }), + ) + }) + + test('throws AbortError when the response is not ok', async () => { + // Given + const overrides = JSON.stringify({templates: {}}) + vi.mocked(shopifyFetch).mockResolvedValue(jsonResponse({}, {status: 422, statusText: 'Unprocessable Entity'})) + + // When/Then + await expect( + createThemePreview({session, storefrontToken: 'sf_token', overridesContent: overrides}), + ).rejects.toThrow('Theme preview request failed with status 422: Unprocessable Entity') + }) + + test('throws AbortError when the response body contains an error', async () => { + // Given + const overrides = JSON.stringify({templates: {}}) + vi.mocked(shopifyFetch).mockResolvedValue( + jsonResponse({url: null, preview_identifier: null, error: 'Invalid template'}), + ) + + // When/Then + await expect( + createThemePreview({session, storefrontToken: 'sf_token', overridesContent: overrides}), + ).rejects.toThrow('Theme preview failed: Invalid template') + }) +}) + +describe('updateThemePreview', () => { + test('POSTs to the preview endpoint with a session identifier and returns url and preview_identifier', async () => { + // Given + const overrides = JSON.stringify({templates: {'index.liquid': '

Hello

'}}) + const responseBody = {url: 'https://abc.shopifypreview.com', preview_identifier: 'abc'} + const expectedSessionIdentifier = '1234-abc' + vi.mocked(shopifyFetch).mockResolvedValue(jsonResponse(responseBody)) + + // When + const result = await updateThemePreview({ + session, + storefrontToken: 'sf_token', + overridesContent: overrides, + previewIdentifier: expectedSessionIdentifier, + }) + + // Then + expect(result).toEqual(responseBody) + expect(shopifyFetch).toHaveBeenCalledWith( + `https://${session.storeFqdn}/theme_preview.json?preview_identifier=${expectedSessionIdentifier}`, + expect.objectContaining({ + method: 'POST', + body: overrides, + headers: expect.objectContaining({ + Authorization: 'Bearer sf_token', + 'Content-Type': 'application/json', + }), + }), + ) + }) + + test('encodes the session identifier in the URL', async () => { + // Given + const overrides = JSON.stringify({templates: {}}) + const responseBody = {url: 'https://abc.shopifypreview.com', preview_identifier: 'abc'} + vi.mocked(shopifyFetch).mockResolvedValue(jsonResponse(responseBody)) + + // When + await updateThemePreview({ + session, + storefrontToken: 'sf_token', + overridesContent: overrides, + previewIdentifier: 'token with spaces&special=chars', + }) + + // Then + expect(shopifyFetch).toHaveBeenCalledWith( + `https://${session.storeFqdn}/theme_preview.json?preview_identifier=token%20with%20spaces%26special%3Dchars`, + expect.any(Object), + ) + }) + + test('throws AbortError when the response is not ok', async () => { + // Given + const overrides = JSON.stringify({templates: {}}) + vi.mocked(shopifyFetch).mockResolvedValue(jsonResponse({}, {status: 422, statusText: 'Unprocessable Entity'})) + + // When/Then + await expect( + updateThemePreview({ + session, + storefrontToken: 'sf_token', + overridesContent: overrides, + previewIdentifier: '1234-abc', + }), + ).rejects.toThrow('Theme preview request failed with status 422: Unprocessable Entity') + }) + + test('throws AbortError when the response body contains an error', async () => { + // Given + const overrides = JSON.stringify({templates: {}}) + vi.mocked(shopifyFetch).mockResolvedValue( + jsonResponse({url: null, preview_identifier: null, error: 'Session expired'}), + ) + + // When/Then + await expect( + updateThemePreview({ + session, + storefrontToken: 'sf_token', + overridesContent: overrides, + previewIdentifier: '1234-abc', + }), + ).rejects.toThrow('Theme preview failed: Session expired') + }) +}) diff --git a/packages/theme/src/cli/utilities/theme-previews/preview.ts b/packages/theme/src/cli/utilities/theme-previews/preview.ts new file mode 100644 index 0000000000..3ed4f3dd5d --- /dev/null +++ b/packages/theme/src/cli/utilities/theme-previews/preview.ts @@ -0,0 +1,110 @@ +import {buildBaseStorefrontUrl} from '../theme-environment/storefront-renderer.js' +import {defaultHeaders} from '../theme-environment/storefront-utils.js' +import {DevServerSession} from '../theme-environment/types.js' +import {recordTiming} from '@shopify/cli-kit/node/analytics' +import {AbortError} from '@shopify/cli-kit/node/error' +import {shopifyFetch, Response} from '@shopify/cli-kit/node/http' + +interface CreateThemePreviewOptions { + session: DevServerSession + storefrontToken: string + overridesContent: string +} + +interface UpdateThemePreviewOptions extends CreateThemePreviewOptions { + previewIdentifier: string +} + +interface ThemePreviewResult { + url: string + preview_identifier: string +} + +interface PreviewResponse { + url?: string + preview_identifier?: string + error?: string +} + +/** + * Creates a theme preview with overrides. + * + * @param options - The options for creating a theme preview. + * @returns The preview URL and identifier for the applied overrides. + */ +export async function createThemePreview({ + session, + storefrontToken, + overridesContent, +}: CreateThemePreviewOptions): Promise { + recordTiming('theme-preview:create') + const baseUrl = buildBaseStorefrontUrl(session) + const url = `${baseUrl}/theme_preview.json` + + const response = await shopifyFetch(url, { + method: 'POST', + body: overridesContent, + headers: { + ...defaultHeaders(), + Authorization: `Bearer ${storefrontToken}`, + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + throw new AbortError(`Theme preview request failed with status ${response.status}: ${response.statusText}`) + } + + const result = await parsePreviewResponse(response) + recordTiming('theme-preview:create') + return result +} + +/** + * Overwrites a theme preview session with new overrides. + * + * @param options - The options for updating a theme preview. + * @returns The preview URL and identifier for the applied overrides. + */ +export async function updateThemePreview({ + session, + storefrontToken, + overridesContent, + previewIdentifier, +}: UpdateThemePreviewOptions): Promise { + recordTiming('theme-preview:update') + const baseUrl = buildBaseStorefrontUrl(session) + const url = `${baseUrl}/theme_preview.json?preview_identifier=${encodeURIComponent(previewIdentifier)}` + + const response = await shopifyFetch(url, { + method: 'POST', + body: overridesContent, + headers: { + ...defaultHeaders(), + Authorization: `Bearer ${storefrontToken}`, + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + throw new AbortError(`Theme preview request failed with status ${response.status}: ${response.statusText}`) + } + + const result = await parsePreviewResponse(response) + recordTiming('theme-preview:update') + return result +} + +async function parsePreviewResponse(response: Response): Promise { + const body = (await response.json()) as PreviewResponse + + if (body.error) { + throw new AbortError(`Theme preview failed: ${body.error}`) + } + + if (!body.url || !body.preview_identifier) { + throw new AbortError('Theme preview returned an unexpected response') + } + + return {url: body.url, preview_identifier: body.preview_identifier} +}