diff --git a/.changeset/nice-radios-lie.md b/.changeset/nice-radios-lie.md new file mode 100644 index 00000000000..ecbd39e09b3 --- /dev/null +++ b/.changeset/nice-radios-lie.md @@ -0,0 +1,6 @@ +--- +'@shopify/cli-kit': patch +'@shopify/theme': patch +--- + +Extract the ownership of development themes diff --git a/.eslintrc.cjs b/.eslintrc.cjs index fe63d847ae8..3d114de215a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -190,6 +190,7 @@ module.exports = { '**/public/node/plugins/tunnel.ts', '**/public/node/environments.ts', '**/public/node/result.ts', + '**/public/node/themes/**/*', ], rules: { 'jsdoc/check-access': 'error', @@ -251,6 +252,7 @@ module.exports = { rules: { '@typescript-eslint/explicit-module-boundary-types': 'error', }, + excludedFiles: ['**/public/node/themes/**/*'], }, { files: ['src/private/node/ui/components/**/*.tsx'], diff --git a/packages/cli-kit/src/public/node/themes/generate-theme-name.test.ts b/packages/cli-kit/src/public/node/themes/generate-theme-name.test.ts new file mode 100644 index 00000000000..f8c1195a702 --- /dev/null +++ b/packages/cli-kit/src/public/node/themes/generate-theme-name.test.ts @@ -0,0 +1,25 @@ +import {generateThemeName} from './generate-theme-name.js' +import {beforeEach, describe, expect, it, vi} from 'vitest' +import {hostname} from 'os' +import {randomBytes} from 'crypto' + +vi.mock('os') +vi.mock('crypto') + +describe('generateThemeName', () => { + const context = 'Development' + + beforeEach(() => { + vi.mocked(randomBytes).mockImplementation(() => Buffer.from([1, 2, 3])) + }) + + it('should not truncate if the theme name is below the API limit', () => { + vi.mocked(hostname).mockReturnValue('Mac-Book-Pro.My-Router') + expect(generateThemeName(context)).toEqual('Development (010203-Mac-Book-Pro)') + }) + + it('should truncate if the theme name is above the API limit', () => { + vi.mocked(hostname).mockReturnValue('theme-dev-lan-very-long-name-that-will-be-truncated') + expect(generateThemeName(context)).toEqual('Development (010203-theme-dev-lan-very-long-name-t)') + }) +}) diff --git a/packages/cli-kit/src/public/node/themes/generate-theme-name.ts b/packages/cli-kit/src/public/node/themes/generate-theme-name.ts new file mode 100644 index 00000000000..34adf137ac9 --- /dev/null +++ b/packages/cli-kit/src/public/node/themes/generate-theme-name.ts @@ -0,0 +1,15 @@ +import {replaceInvalidCharacters} from './replace-invalid-characters.js' +import {randomBytes} from 'crypto' +import {hostname} from 'os' + +const API_NAME_LIMIT = 50 + +export function generateThemeName(context: string): string { + const hostNameWithoutDomain = hostname().split('.')[0]! + const hash = randomBytes(3).toString('hex') + + const name = `${context} ()` + const hostNameCharacterLimit = API_NAME_LIMIT - name.length - hash.length + const identifier = replaceInvalidCharacters(`${hash}-${hostNameWithoutDomain.substring(0, hostNameCharacterLimit)}`) + return `${context} (${identifier})` +} diff --git a/packages/theme/src/cli/models/theme.ts b/packages/cli-kit/src/public/node/themes/models/theme.ts similarity index 63% rename from packages/theme/src/cli/models/theme.ts rename to packages/cli-kit/src/public/node/themes/models/theme.ts index 2f3ab221650..df8a68fe03a 100644 --- a/packages/theme/src/cli/models/theme.ts +++ b/packages/cli-kit/src/public/node/themes/models/theme.ts @@ -1,5 +1,7 @@ +export const DEVELOPMENT_THEME_ROLE = 'development' + export class Theme { - constructor(public id: number, public name: string, private _role: string) {} + constructor(public id: number, public name: string, private _role: string, public createdAtRuntime = false) {} get role(): string { if (this._role === 'main') { @@ -16,4 +18,8 @@ export class Theme { this._role = _role } } + + get hasDevelopmentRole(): boolean { + return this.role === DEVELOPMENT_THEME_ROLE + } } diff --git a/packages/cli-kit/src/public/node/themes/replace-invalid-characters.test.ts b/packages/cli-kit/src/public/node/themes/replace-invalid-characters.test.ts new file mode 100644 index 00000000000..f878780fdd0 --- /dev/null +++ b/packages/cli-kit/src/public/node/themes/replace-invalid-characters.test.ts @@ -0,0 +1,14 @@ +import {replaceInvalidCharacters} from './replace-invalid-characters.js' +import {describe, expect, it} from 'vitest' + +describe('replaceInvalidCharacters', () => { + it('should replace unused ASCII characters', () => { + const asciiStringChar = '\x8F' + expect(replaceInvalidCharacters(`theme-dev-${asciiStringChar}.lan`)).toEqual('theme-dev---lan') + }) + + it('should not replace non-latin letters and marks', () => { + const hostName = 'ÇaVaこんにちはПривіт' + expect(replaceInvalidCharacters(hostName)).toEqual(hostName) + }) +}) diff --git a/packages/cli-kit/src/public/node/themes/replace-invalid-characters.ts b/packages/cli-kit/src/public/node/themes/replace-invalid-characters.ts new file mode 100644 index 00000000000..0d4b09be948 --- /dev/null +++ b/packages/cli-kit/src/public/node/themes/replace-invalid-characters.ts @@ -0,0 +1,8 @@ +export function replaceInvalidCharacters(identifier: string) { + const findAllMatches = 'g' + const enablesUnicodeSupport = 'u' + return identifier.replace( + new RegExp(/[^\p{Letter}\p{Number}\p{Mark}-]/, `${findAllMatches}${enablesUnicodeSupport}`), + '-', + ) +} diff --git a/packages/cli-kit/src/public/node/themes/theme-manager.ts b/packages/cli-kit/src/public/node/themes/theme-manager.ts new file mode 100644 index 00000000000..baa70618622 --- /dev/null +++ b/packages/cli-kit/src/public/node/themes/theme-manager.ts @@ -0,0 +1,50 @@ +import {fetchTheme, createTheme} from './themes-api.js' +import {DEVELOPMENT_THEME_ROLE, Theme} from './models/theme.js' +import {generateThemeName} from './generate-theme-name.js' +import {AdminSession} from '@shopify/cli-kit/node/session' +import {BugError} from '@shopify/cli-kit/node/error' + +export abstract class ThemeManager { + protected themeId: string | undefined + protected abstract setTheme(themeId: string): void + protected abstract removeTheme(): void + protected abstract context: string + + constructor(protected adminSession: AdminSession) {} + + async findOrCreate(): Promise { + let theme = await this.fetch() + if (!theme) { + theme = await this.create() + } + return theme + } + + protected async fetch() { + if (!this.themeId) { + return + } + const theme = await fetchTheme(parseInt(this.themeId, 10), this.adminSession) + if (!theme) { + this.removeTheme() + } + return theme + } + + private async create() { + const name = generateThemeName(this.context) + const role = DEVELOPMENT_THEME_ROLE + const theme = await createTheme( + { + name, + role, + }, + this.adminSession, + ) + if (!theme) { + throw new BugError(`Could not create theme with name "${name}" and role "${role}"`) + } + this.setTheme(theme.id.toString()) + return theme + } +} diff --git a/packages/theme/src/cli/utilities/theme-urls.test.ts b/packages/cli-kit/src/public/node/themes/theme-urls.test.ts similarity index 96% rename from packages/theme/src/cli/utilities/theme-urls.test.ts rename to packages/cli-kit/src/public/node/themes/theme-urls.test.ts index c9129fb8013..fdc0deda9bb 100644 --- a/packages/theme/src/cli/utilities/theme-urls.test.ts +++ b/packages/cli-kit/src/public/node/themes/theme-urls.test.ts @@ -1,5 +1,5 @@ import {storeAdminUrl, themeEditorUrl, themePreviewUrl} from './theme-urls.js' -import {Theme} from '../models/theme.js' +import {Theme} from './models/theme.js' import {test, describe, expect} from 'vitest' const session = {token: 'token', storeFqdn: 'my-shop.myshopify.com'} diff --git a/packages/theme/src/cli/utilities/theme-urls.ts b/packages/cli-kit/src/public/node/themes/theme-urls.ts similarity index 90% rename from packages/theme/src/cli/utilities/theme-urls.ts rename to packages/cli-kit/src/public/node/themes/theme-urls.ts index 520f8962fbb..cc331e91d79 100644 --- a/packages/theme/src/cli/utilities/theme-urls.ts +++ b/packages/cli-kit/src/public/node/themes/theme-urls.ts @@ -1,4 +1,4 @@ -import {Theme} from '../models/theme.js' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' import {AdminSession} from '@shopify/cli-kit/node/session' export function themePreviewUrl(theme: Theme, session: AdminSession) { diff --git a/packages/theme/src/cli/utilities/themes-api.test.ts b/packages/cli-kit/src/public/node/themes/themes-api.test.ts similarity index 100% rename from packages/theme/src/cli/utilities/themes-api.test.ts rename to packages/cli-kit/src/public/node/themes/themes-api.test.ts diff --git a/packages/theme/src/cli/utilities/themes-api.ts b/packages/cli-kit/src/public/node/themes/themes-api.ts similarity index 91% rename from packages/theme/src/cli/utilities/themes-api.ts rename to packages/cli-kit/src/public/node/themes/themes-api.ts index a66f835a851..611deba2690 100644 --- a/packages/theme/src/cli/utilities/themes-api.ts +++ b/packages/cli-kit/src/public/node/themes/themes-api.ts @@ -2,13 +2,18 @@ import * as throttler from './themes-api/throttler.js' import {apiCallLimit, retryAfter} from './themes-api/headers.js' import {retry} from './themes-api/retry.js' import {storeAdminUrl} from './theme-urls.js' -import {Theme} from '../models/theme.js' +import {Theme} from './models/theme.js' import {restRequest, RestResponse} from '@shopify/cli-kit/node/api/admin' import {AdminSession} from '@shopify/cli-kit/node/session' import {AbortError} from '@shopify/cli-kit/node/error' export type ThemeParams = Partial> +export async function fetchTheme(id: number, session: AdminSession): Promise { + const response = await request('GET', `/themes/${id}`, session, undefined, {fields: 'id'}) + return buildTheme(response.json.theme) +} + export async function fetchThemes(session: AdminSession): Promise { const response = await request('GET', '/themes', session, undefined, {fields: 'id,name,role'}) return buildThemes(response) @@ -16,7 +21,7 @@ export async function fetchThemes(session: AdminSession): Promise { export async function createTheme(params: ThemeParams, session: AdminSession): Promise { const response = await request('POST', '/themes', session, {theme: {...params}}) - return buildTheme(response.json.theme) + return buildTheme({...response.json.theme, createdAtRuntime: true}) } export async function updateTheme(id: number, params: ThemeParams, session: AdminSession): Promise { @@ -87,7 +92,7 @@ function buildTheme(themeJson: any): Theme | undefined { return undefined } - return new Theme(themeJson.id, themeJson.name, themeJson.role) + return new Theme(themeJson.id, themeJson.name, themeJson.role, themeJson.createdAtRuntime) } function handleForbiddenError(session: AdminSession): never { diff --git a/packages/theme/src/cli/utilities/themes-api/headers.test.ts b/packages/cli-kit/src/public/node/themes/themes-api/headers.test.ts similarity index 100% rename from packages/theme/src/cli/utilities/themes-api/headers.test.ts rename to packages/cli-kit/src/public/node/themes/themes-api/headers.test.ts diff --git a/packages/theme/src/cli/utilities/themes-api/headers.ts b/packages/cli-kit/src/public/node/themes/themes-api/headers.ts similarity index 100% rename from packages/theme/src/cli/utilities/themes-api/headers.ts rename to packages/cli-kit/src/public/node/themes/themes-api/headers.ts diff --git a/packages/theme/src/cli/utilities/themes-api/retry.ts b/packages/cli-kit/src/public/node/themes/themes-api/retry.ts similarity index 100% rename from packages/theme/src/cli/utilities/themes-api/retry.ts rename to packages/cli-kit/src/public/node/themes/themes-api/retry.ts diff --git a/packages/theme/src/cli/utilities/themes-api/throttler.ts b/packages/cli-kit/src/public/node/themes/themes-api/throttler.ts similarity index 100% rename from packages/theme/src/cli/utilities/themes-api/throttler.ts rename to packages/cli-kit/src/public/node/themes/themes-api/throttler.ts diff --git a/packages/theme/oclif.manifest.json b/packages/theme/oclif.manifest.json index d7668777666..d6199455162 100644 --- a/packages/theme/oclif.manifest.json +++ b/packages/theme/oclif.manifest.json @@ -1 +1 @@ -{"version":"3.39.0","commands":{"theme:check":{"id":"theme:check","description":"Validate the theme.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."},"auto-correct":{"name":"auto-correct","type":"boolean","char":"a","description":"Automatically fix offenses","required":false,"allowNo":false},"category":{"name":"category","type":"option","char":"c","description":"Only run this category of checks\nRuns checks matching all categories when specified more than once","required":false,"multiple":false},"config":{"name":"config","type":"option","char":"C","description":"Use the config provided, overriding .theme-check.yml if present\nUse :theme_app_extension to use default checks for theme app extensions","required":false,"multiple":false},"exclude-category":{"name":"exclude-category","type":"option","char":"x","description":"Exclude this category of checks\nExcludes checks matching any category when specified more than once","required":false,"multiple":false},"fail-level":{"name":"fail-level","type":"option","description":"Minimum severity for exit with error code","required":false,"multiple":false,"options":["error","suggestion","style"]},"init":{"name":"init","type":"boolean","description":"Generate a .theme-check.yml file","required":false,"allowNo":false},"list":{"name":"list","type":"boolean","description":"List enabled checks","required":false,"allowNo":false},"output":{"name":"output","type":"option","char":"o","description":"The output format to use","required":false,"multiple":false,"options":["text","json"],"default":"text"},"print":{"name":"print","type":"boolean","description":"Output active config to STDOUT","required":false,"allowNo":false},"version":{"name":"version","type":"boolean","char":"v","description":"Print Theme Check version","required":false,"allowNo":false}},"args":[],"cli2Flags":["auto-correct","category","config","exclude-category","fail-level","init","list","output","print","version"]},"theme:delete":{"id":"theme:delete","description":"Delete remote themes from the connected store. This command can't be undone.","strict":false,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false},"development":{"name":"development","type":"boolean","char":"d","description":"Delete your development theme.","allowNo":false},"show-all":{"name":"show-all","type":"boolean","char":"a","description":"Include others development themes in theme list.","allowNo":false},"force":{"name":"force","type":"boolean","char":"f","description":"Skip confirmation.","allowNo":false},"theme":{"name":"theme","type":"option","char":"t","description":"Theme ID or name of the remote theme.","multiple":true},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false}},"args":[]},"theme:dev":{"id":"theme:dev","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.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."},"host":{"name":"host","type":"option","description":"Set which network interface the web server listens on. The default value is 127.0.0.1.","multiple":false},"live-reload":{"name":"live-reload","type":"option","description":"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","multiple":false,"options":["hot-reload","full-page","off"],"default":"hot-reload"},"poll":{"name":"poll","type":"boolean","description":"Force polling to detect file changes.","allowNo":false},"theme-editor-sync":{"name":"theme-editor-sync","type":"boolean","char":"e","description":"Synchronize Theme Editor updates in the local theme files.","allowNo":false},"port":{"name":"port","type":"option","description":"Local port to serve theme preview from.","multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false},"theme":{"name":"theme","type":"option","char":"t","description":"Theme ID or name of the remote theme.","multiple":false},"only":{"name":"only","type":"option","char":"o","description":"Hot reload only files that match the specified pattern.","multiple":true},"ignore":{"name":"ignore","type":"option","char":"x","description":"Skip hot reloading any files that match the specified pattern.","multiple":true},"stable":{"name":"stable","type":"boolean","description":"Performs the upload by relying in the legacy upload approach (slower, but it might be more stable in some scenarios)","hidden":true,"allowNo":false},"force":{"name":"force","type":"boolean","char":"f","description":"Proceed without confirmation, if current directory does not seem to be theme directory.","hidden":true,"allowNo":false},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false}},"args":[],"cli2Flags":["host","live-reload","poll","theme-editor-sync","port","theme","only","ignore","stable","force"]},"theme:help-old":{"id":"theme:help-old","description":"Show help from Ruby CLI","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","hidden":true,"aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"command":{"name":"command","type":"option","description":"The command for which to show CLI2 help.","multiple":false}},"args":[]},"theme:info":{"id":"theme:info","description":"Print basic information about your theme environment.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false}},"args":[]},"theme:init":{"id":"theme:init","description":"Clones a Git repository to use as a starting point for building a new theme.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."},"clone-url":{"name":"clone-url","type":"option","char":"u","description":"The Git URL to clone from. Defaults to Shopify's example theme, Dawn: https://github.com/Shopify/dawn.git","multiple":false,"default":"https://github.com/Shopify/dawn.git"},"latest":{"name":"latest","type":"boolean","char":"l","description":"Downloads the latest release of the `clone-url`","allowNo":false}},"args":[{"name":"name","description":"Name of the new theme","required":false}]},"theme:language-server":{"id":"theme:language-server","description":"Start a Language Server Protocol server.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false}},"args":[]},"theme:list":{"id":"theme:list","description":"Lists your remote themes.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false},"role":{"name":"role","type":"option","description":"Only list themes with the given role.","helpValue":"(live|unpublished|development)","multiple":false,"options":["live","unpublished","development"]},"name":{"name":"name","type":"option","description":"Only list themes that contain the given name.","multiple":false},"id":{"name":"id","type":"option","description":"Only list theme with the given ID.","multiple":false}},"args":[]},"theme:open":{"id":"theme:open","description":"Opens the preview of your remote theme.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false},"development":{"name":"development","type":"boolean","char":"d","description":"Delete your development theme.","allowNo":false},"editor":{"name":"editor","type":"boolean","char":"e","description":"Open the theme editor for the specified theme in the browser.","allowNo":false},"live":{"name":"live","type":"boolean","char":"l","description":"Pull theme files from your remote live theme.","allowNo":false},"theme":{"name":"theme","type":"option","char":"t","description":"Theme ID or name of the remote theme.","multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false}},"args":[]},"theme:package":{"id":"theme:package","description":"Package your theme into a .zip file, ready to upload to the Online Store.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."}},"args":[]},"theme:publish":{"id":"theme:publish","description":"Set a remote theme as the live theme.","strict":false,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false},"force":{"name":"force","type":"boolean","char":"f","description":"Skip confirmation.","allowNo":false},"theme":{"name":"theme","type":"option","char":"t","description":"Theme ID or name of the remote theme.","multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false}},"args":[]},"theme:pull":{"id":"theme:pull","description":"Download your remote theme files locally.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false},"theme":{"name":"theme","type":"option","char":"t","description":"Theme ID or name of the remote theme.","multiple":false},"development":{"name":"development","type":"boolean","char":"d","description":"Pull theme files from your remote development theme.","allowNo":false},"live":{"name":"live","type":"boolean","char":"l","description":"Pull theme files from your remote live theme.","allowNo":false},"nodelete":{"name":"nodelete","type":"boolean","char":"n","description":"Runs the pull command without deleting local files.","allowNo":false},"only":{"name":"only","type":"option","char":"o","description":"Download only the specified files (Multiple flags allowed).","multiple":true},"ignore":{"name":"ignore","type":"option","char":"x","description":"Skip downloading the specified files (Multiple flags allowed).","multiple":true},"force":{"name":"force","type":"boolean","char":"f","description":"Proceed without confirmation, if current directory does not seem to be theme directory.","hidden":true,"allowNo":false}},"args":[],"cli2Flags":["theme","development","live","nodelete","only","ignore","force"]},"theme:push":{"id":"theme:push","description":"Uploads your local theme files to the connected store, overwriting the remote version if specified.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false},"theme":{"name":"theme","type":"option","char":"t","description":"Theme ID or name of the remote theme.","multiple":false},"development":{"name":"development","type":"boolean","char":"d","description":"Push theme files from your remote development theme.","allowNo":false},"live":{"name":"live","type":"boolean","char":"l","description":"Push theme files from your remote live theme.","allowNo":false},"unpublished":{"name":"unpublished","type":"boolean","char":"u","description":"Create a new unpublished theme and push to it.","allowNo":false},"nodelete":{"name":"nodelete","type":"boolean","char":"n","description":"Runs the push command without deleting local files.","allowNo":false},"only":{"name":"only","type":"option","char":"o","description":"Download only the specified files (Multiple flags allowed).","multiple":true},"ignore":{"name":"ignore","type":"option","char":"x","description":"Skip downloading the specified files (Multiple flags allowed).","multiple":true},"json":{"name":"json","type":"boolean","char":"j","description":"Output JSON instead of a UI.","allowNo":false},"allow-live":{"name":"allow-live","type":"boolean","char":"a","description":"Allow push to a live theme.","allowNo":false},"publish":{"name":"publish","type":"boolean","char":"p","description":"Publish as the live theme after uploading.","allowNo":false},"stable":{"name":"stable","type":"boolean","description":"Performs the upload by relying in the legacy upload approach (slower, but it might be more stable in some scenarios)","hidden":true,"allowNo":false},"force":{"name":"force","type":"boolean","char":"f","description":"Proceed without confirmation, if current directory does not seem to be theme directory.","hidden":true,"allowNo":false}},"args":[],"cli2Flags":["theme","development","live","unpublished","nodelete","only","ignore","json","allow-live","publish","stable","force"]},"theme:serve":{"id":"theme:serve","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.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","hidden":true,"aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."},"host":{"name":"host","type":"option","description":"Set which network interface the web server listens on. The default value is 127.0.0.1.","multiple":false},"live-reload":{"name":"live-reload","type":"option","description":"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","multiple":false,"options":["hot-reload","full-page","off"],"default":"hot-reload"},"poll":{"name":"poll","type":"boolean","description":"Force polling to detect file changes.","allowNo":false},"theme-editor-sync":{"name":"theme-editor-sync","type":"boolean","char":"e","description":"Synchronize Theme Editor updates in the local theme files.","allowNo":false},"port":{"name":"port","type":"option","description":"Local port to serve theme preview from.","multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false},"theme":{"name":"theme","type":"option","char":"t","description":"Theme ID or name of the remote theme.","multiple":false},"only":{"name":"only","type":"option","char":"o","description":"Hot reload only files that match the specified pattern.","multiple":true},"ignore":{"name":"ignore","type":"option","char":"x","description":"Skip hot reloading any files that match the specified pattern.","multiple":true},"stable":{"name":"stable","type":"boolean","description":"Performs the upload by relying in the legacy upload approach (slower, but it might be more stable in some scenarios)","hidden":true,"allowNo":false},"force":{"name":"force","type":"boolean","char":"f","description":"Proceed without confirmation, if current directory does not seem to be theme directory.","hidden":true,"allowNo":false},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false}},"args":[]},"theme:share":{"id":"theme:share","description":"Creates a shareable, unpublished, and new theme on your theme library with a randomized name. Works like an alias to {{command:theme push -u -t=RANDOMIZED_NAME}}.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false},"force":{"name":"force","type":"boolean","char":"f","description":"Proceed without confirmation, if current directory does not seem to be theme directory.","hidden":true,"allowNo":false}},"args":[],"cli2Flags":["force"]}}} \ No newline at end of file +{"version":"3.39.0","commands":{"theme:check":{"id":"theme:check","description":"Validate the theme.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."},"auto-correct":{"name":"auto-correct","type":"boolean","char":"a","description":"Automatically fix offenses","required":false,"allowNo":false},"category":{"name":"category","type":"option","char":"c","description":"Only run this category of checks\nRuns checks matching all categories when specified more than once","required":false,"multiple":false},"config":{"name":"config","type":"option","char":"C","description":"Use the config provided, overriding .theme-check.yml if present\nUse :theme_app_extension to use default checks for theme app extensions","required":false,"multiple":false},"exclude-category":{"name":"exclude-category","type":"option","char":"x","description":"Exclude this category of checks\nExcludes checks matching any category when specified more than once","required":false,"multiple":false},"fail-level":{"name":"fail-level","type":"option","description":"Minimum severity for exit with error code","required":false,"multiple":false,"options":["error","suggestion","style"]},"init":{"name":"init","type":"boolean","description":"Generate a .theme-check.yml file","required":false,"allowNo":false},"list":{"name":"list","type":"boolean","description":"List enabled checks","required":false,"allowNo":false},"output":{"name":"output","type":"option","char":"o","description":"The output format to use","required":false,"multiple":false,"options":["text","json"],"default":"text"},"print":{"name":"print","type":"boolean","description":"Output active config to STDOUT","required":false,"allowNo":false},"version":{"name":"version","type":"boolean","char":"v","description":"Print Theme Check version","required":false,"allowNo":false}},"args":[],"cli2Flags":["auto-correct","category","config","exclude-category","fail-level","init","list","output","print","version"]},"theme:delete":{"id":"theme:delete","description":"Delete remote themes from the connected store. This command can't be undone.","strict":false,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false},"development":{"name":"development","type":"boolean","char":"d","description":"Delete your development theme.","allowNo":false},"show-all":{"name":"show-all","type":"boolean","char":"a","description":"Include others development themes in theme list.","allowNo":false},"force":{"name":"force","type":"boolean","char":"f","description":"Skip confirmation.","allowNo":false},"theme":{"name":"theme","type":"option","char":"t","description":"Theme ID or name of the remote theme.","multiple":true},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false}},"args":[]},"theme:dev":{"id":"theme:dev","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.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."},"host":{"name":"host","type":"option","description":"Set which network interface the web server listens on. The default value is 127.0.0.1.","multiple":false},"live-reload":{"name":"live-reload","type":"option","description":"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","multiple":false,"options":["hot-reload","full-page","off"],"default":"hot-reload"},"poll":{"name":"poll","type":"boolean","description":"Force polling to detect file changes.","allowNo":false},"theme-editor-sync":{"name":"theme-editor-sync","type":"boolean","char":"e","description":"Synchronize Theme Editor updates in the local theme files.","allowNo":false},"port":{"name":"port","type":"option","description":"Local port to serve theme preview from.","multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false},"theme":{"name":"theme","type":"option","char":"t","description":"Theme ID or name of the remote theme.","multiple":false},"only":{"name":"only","type":"option","char":"o","description":"Hot reload only files that match the specified pattern.","multiple":true},"ignore":{"name":"ignore","type":"option","char":"x","description":"Skip hot reloading any files that match the specified pattern.","multiple":true},"stable":{"name":"stable","type":"boolean","description":"Performs the upload by relying in the legacy upload approach (slower, but it might be more stable in some scenarios)","hidden":true,"allowNo":false},"force":{"name":"force","type":"boolean","char":"f","description":"Proceed without confirmation, if current directory does not seem to be theme directory.","hidden":true,"allowNo":false},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false}},"args":[],"cli2Flags":["host","live-reload","poll","theme-editor-sync","overwrite-json","port","theme","only","ignore","stable","force"]},"theme:help-old":{"id":"theme:help-old","description":"Show help from Ruby CLI","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","hidden":true,"aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"command":{"name":"command","type":"option","description":"The command for which to show CLI2 help.","multiple":false}},"args":[]},"theme:info":{"id":"theme:info","description":"Print basic information about your theme environment.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false}},"args":[]},"theme:init":{"id":"theme:init","description":"Clones a Git repository to use as a starting point for building a new theme.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."},"clone-url":{"name":"clone-url","type":"option","char":"u","description":"The Git URL to clone from. Defaults to Shopify's example theme, Dawn: https://github.com/Shopify/dawn.git","multiple":false,"default":"https://github.com/Shopify/dawn.git"},"latest":{"name":"latest","type":"boolean","char":"l","description":"Downloads the latest release of the `clone-url`","allowNo":false}},"args":[{"name":"name","description":"Name of the new theme","required":false}]},"theme:language-server":{"id":"theme:language-server","description":"Start a Language Server Protocol server.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false}},"args":[]},"theme:list":{"id":"theme:list","description":"Lists your remote themes.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false},"role":{"name":"role","type":"option","description":"Only list themes with the given role.","helpValue":"(live|unpublished|development)","multiple":false,"options":["live","unpublished","development"]},"name":{"name":"name","type":"option","description":"Only list themes that contain the given name.","multiple":false},"id":{"name":"id","type":"option","description":"Only list theme with the given ID.","multiple":false}},"args":[]},"theme:open":{"id":"theme:open","description":"Opens the preview of your remote theme.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false},"development":{"name":"development","type":"boolean","char":"d","description":"Delete your development theme.","allowNo":false},"editor":{"name":"editor","type":"boolean","char":"e","description":"Open the theme editor for the specified theme in the browser.","allowNo":false},"live":{"name":"live","type":"boolean","char":"l","description":"Pull theme files from your remote live theme.","allowNo":false},"theme":{"name":"theme","type":"option","char":"t","description":"Theme ID or name of the remote theme.","multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false}},"args":[]},"theme:package":{"id":"theme:package","description":"Package your theme into a .zip file, ready to upload to the Online Store.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."}},"args":[]},"theme:publish":{"id":"theme:publish","description":"Set a remote theme as the live theme.","strict":false,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false},"force":{"name":"force","type":"boolean","char":"f","description":"Skip confirmation.","allowNo":false},"theme":{"name":"theme","type":"option","char":"t","description":"Theme ID or name of the remote theme.","multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false}},"args":[]},"theme:pull":{"id":"theme:pull","description":"Download your remote theme files locally.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false},"theme":{"name":"theme","type":"option","char":"t","description":"Theme ID or name of the remote theme.","multiple":false},"development":{"name":"development","type":"boolean","char":"d","description":"Pull theme files from your remote development theme.","allowNo":false},"live":{"name":"live","type":"boolean","char":"l","description":"Pull theme files from your remote live theme.","allowNo":false},"nodelete":{"name":"nodelete","type":"boolean","char":"n","description":"Runs the pull command without deleting local files.","allowNo":false},"only":{"name":"only","type":"option","char":"o","description":"Download only the specified files (Multiple flags allowed).","multiple":true},"ignore":{"name":"ignore","type":"option","char":"x","description":"Skip downloading the specified files (Multiple flags allowed).","multiple":true},"force":{"name":"force","type":"boolean","char":"f","description":"Proceed without confirmation, if current directory does not seem to be theme directory.","hidden":true,"allowNo":false}},"args":[],"cli2Flags":["theme","development","live","nodelete","only","ignore","force"]},"theme:push":{"id":"theme:push","description":"Uploads your local theme files to the connected store, overwriting the remote version if specified.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false},"theme":{"name":"theme","type":"option","char":"t","description":"Theme ID or name of the remote theme.","multiple":false},"development":{"name":"development","type":"boolean","char":"d","description":"Push theme files from your remote development theme.","allowNo":false},"live":{"name":"live","type":"boolean","char":"l","description":"Push theme files from your remote live theme.","allowNo":false},"unpublished":{"name":"unpublished","type":"boolean","char":"u","description":"Create a new unpublished theme and push to it.","allowNo":false},"nodelete":{"name":"nodelete","type":"boolean","char":"n","description":"Runs the push command without deleting local files.","allowNo":false},"only":{"name":"only","type":"option","char":"o","description":"Download only the specified files (Multiple flags allowed).","multiple":true},"ignore":{"name":"ignore","type":"option","char":"x","description":"Skip downloading the specified files (Multiple flags allowed).","multiple":true},"json":{"name":"json","type":"boolean","char":"j","description":"Output JSON instead of a UI.","allowNo":false},"allow-live":{"name":"allow-live","type":"boolean","char":"a","description":"Allow push to a live theme.","allowNo":false},"publish":{"name":"publish","type":"boolean","char":"p","description":"Publish as the live theme after uploading.","allowNo":false},"stable":{"name":"stable","type":"boolean","description":"Performs the upload by relying in the legacy upload approach (slower, but it might be more stable in some scenarios)","hidden":true,"allowNo":false},"force":{"name":"force","type":"boolean","char":"f","description":"Proceed without confirmation, if current directory does not seem to be theme directory.","hidden":true,"allowNo":false}},"args":[],"cli2Flags":["theme","development","live","unpublished","nodelete","only","ignore","json","allow-live","publish","stable","force"]},"theme:serve":{"id":"theme:serve","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.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","hidden":true,"aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."},"host":{"name":"host","type":"option","description":"Set which network interface the web server listens on. The default value is 127.0.0.1.","multiple":false},"live-reload":{"name":"live-reload","type":"option","description":"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","multiple":false,"options":["hot-reload","full-page","off"],"default":"hot-reload"},"poll":{"name":"poll","type":"boolean","description":"Force polling to detect file changes.","allowNo":false},"theme-editor-sync":{"name":"theme-editor-sync","type":"boolean","char":"e","description":"Synchronize Theme Editor updates in the local theme files.","allowNo":false},"port":{"name":"port","type":"option","description":"Local port to serve theme preview from.","multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false},"theme":{"name":"theme","type":"option","char":"t","description":"Theme ID or name of the remote theme.","multiple":false},"only":{"name":"only","type":"option","char":"o","description":"Hot reload only files that match the specified pattern.","multiple":true},"ignore":{"name":"ignore","type":"option","char":"x","description":"Skip hot reloading any files that match the specified pattern.","multiple":true},"stable":{"name":"stable","type":"boolean","description":"Performs the upload by relying in the legacy upload approach (slower, but it might be more stable in some scenarios)","hidden":true,"allowNo":false},"force":{"name":"force","type":"boolean","char":"f","description":"Proceed without confirmation, if current directory does not seem to be theme directory.","hidden":true,"allowNo":false},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false}},"args":[]},"theme:share":{"id":"theme:share","description":"Creates a shareable, unpublished, and new theme on your theme library with a randomized name. Works like an alias to {{command:theme push -u -t=RANDOMIZED_NAME}}.","strict":true,"pluginName":"@shopify/theme","pluginAlias":"@shopify/theme","pluginType":"core","aliases":[],"flags":{"environment":{"name":"environment","type":"option","char":"e","description":"The environment to apply to the current command.","hidden":true,"multiple":false},"verbose":{"name":"verbose","type":"boolean","description":"Increase the verbosity of the logs.","hidden":false,"allowNo":false},"path":{"name":"path","type":"option","description":"The path to your theme directory.","hidden":false,"multiple":false,"default":"."},"password":{"name":"password","type":"option","description":"Password generated from the Theme Access app.","hidden":false,"multiple":false},"store":{"name":"store","type":"option","char":"s","description":"Store URL. It can be the store prefix (johns-apparel) or the full myshopify.com URL (johns-apparel.myshopify.com, https://johns-apparel.myshopify.com).","multiple":false},"force":{"name":"force","type":"boolean","char":"f","description":"Proceed without confirmation, if current directory does not seem to be theme directory.","hidden":true,"allowNo":false}},"args":[],"cli2Flags":["force"]}}} \ No newline at end of file diff --git a/packages/theme/src/cli/commands/theme/dev.ts b/packages/theme/src/cli/commands/theme/dev.ts index dda419b8ea2..fcd32a7d42c 100644 --- a/packages/theme/src/cli/commands/theme/dev.ts +++ b/packages/theme/src/cli/commands/theme/dev.ts @@ -1,11 +1,12 @@ import {themeFlags} from '../../flags.js' import {ensureThemeStore} from '../../utilities/theme-store.js' import ThemeCommand from '../../utilities/theme-command.js' +import {DevelopmentThemeManager} from '../../utilities/development-theme-manager.js' import {Flags} from '@oclif/core' import {globalFlags} from '@shopify/cli-kit/node/cli' import {execCLI2} from '@shopify/cli-kit/node/ruby' import {AbortController} from '@shopify/cli-kit/node/abort' -import {ensureAuthenticatedStorefront, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' +import {AdminSession, ensureAuthenticatedStorefront, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' import {sleep} from '@shopify/cli-kit/node/system' import {outputDebug} from '@shopify/cli-kit/node/output' @@ -80,6 +81,7 @@ export default class Dev extends ThemeCommand { 'live-reload', 'poll', 'theme-editor-sync', + 'overwrite-json', 'port', 'theme', 'only', @@ -96,13 +98,19 @@ export default class Dev extends ThemeCommand { * Every 110 minutes, it will refresh the session token and restart the server. */ async run(): Promise { - const {flags} = await this.parse(Dev) + let {flags} = await this.parse(Dev) + const store = ensureThemeStore(flags) + const adminSession = await ensureAuthenticatedThemes(store, flags.password, [], true) + const theme = await new DevelopmentThemeManager(adminSession).findOrCreate() + flags = { + ...flags, + theme: theme.id.toString(), + 'overwrite-json': Boolean(flags['theme-editor-sync']) && theme.createdAtRuntime, + } const flagsToPass = this.passThroughFlags(flags, {allowedFlags: Dev.cli2Flags}) const command = ['theme', 'serve', flags.path, ...flagsToPass] - const store = ensureThemeStore(flags) - let controller = new AbortController() setInterval(() => { @@ -110,16 +118,20 @@ export default class Dev extends ThemeCommand { controller.abort() controller = new AbortController() // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.execute(store, flags.password, command, controller) + this.execute(adminSession, flags.password, command, controller) }, this.ThemeRefreshTimeoutInMs) // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.execute(store, flags.password, command, controller) + this.execute(adminSession, flags.password, command, controller) } - async execute(store: string, password: string | undefined, command: string[], controller: AbortController) { + async execute( + adminSession: AdminSession, + password: string | undefined, + command: string[], + controller: AbortController, + ) { await sleep(3) - const adminSession = await ensureAuthenticatedThemes(store, password, [], true) const storefrontToken = await ensureAuthenticatedStorefront([], password) return execCLI2(command, {adminSession, storefrontToken, signal: controller.signal}) } diff --git a/packages/theme/src/cli/commands/theme/pull.ts b/packages/theme/src/cli/commands/theme/pull.ts index d0c4a43dfc0..1b8b3b58d4e 100644 --- a/packages/theme/src/cli/commands/theme/pull.ts +++ b/packages/theme/src/cli/commands/theme/pull.ts @@ -1,6 +1,7 @@ import {themeFlags} from '../../flags.js' import {ensureThemeStore} from '../../utilities/theme-store.js' import ThemeCommand from '../../utilities/theme-command.js' +import {DevelopmentThemeManager} from '../../utilities/development-theme-manager.js' import {Flags} from '@oclif/core' import {globalFlags} from '@shopify/cli-kit/node/cli' import {execCLI2} from '@shopify/cli-kit/node/ruby' @@ -56,7 +57,18 @@ export default class Pull extends ThemeCommand { static cli2Flags = ['theme', 'development', 'live', 'nodelete', 'only', 'ignore', 'force'] async run(): Promise { - const {flags} = await this.parse(Pull) + let {flags} = await this.parse(Pull) + const store = ensureThemeStore(flags) + const adminSession = await ensureAuthenticatedThemes(store, flags.password) + + if (flags.development) { + const theme = await new DevelopmentThemeManager(adminSession).find() + flags = { + ...flags, + development: false, + theme: theme.id.toString(), + } + } let validPath = flags.path if (!isAbsolutePath(validPath)) { @@ -67,8 +79,6 @@ export default class Pull extends ThemeCommand { const command = ['theme', 'pull', validPath, ...flagsToPass] - const store = ensureThemeStore(flags) - const adminSession = await ensureAuthenticatedThemes(store, flags.password) await execCLI2(command, {adminSession}) } } diff --git a/packages/theme/src/cli/commands/theme/push.ts b/packages/theme/src/cli/commands/theme/push.ts index 8b699035bda..4fd35af529b 100644 --- a/packages/theme/src/cli/commands/theme/push.ts +++ b/packages/theme/src/cli/commands/theme/push.ts @@ -1,6 +1,7 @@ import {themeFlags} from '../../flags.js' import {ensureThemeStore} from '../../utilities/theme-store.js' import ThemeCommand from '../../utilities/theme-command.js' +import {DevelopmentThemeManager} from '../../utilities/development-theme-manager.js' import {Flags} from '@oclif/core' import {globalFlags} from '@shopify/cli-kit/node/cli' import {execCLI2} from '@shopify/cli-kit/node/ruby' @@ -95,13 +96,22 @@ export default class Push extends ThemeCommand { ] async run(): Promise { - const {flags} = await this.parse(Push) + let {flags} = await this.parse(Push) + const store = ensureThemeStore(flags) + const adminSession = await ensureAuthenticatedThemes(store, flags.password) + + if (flags.development) { + const theme = await new DevelopmentThemeManager(adminSession).findOrCreate() + flags = { + ...flags, + development: false, + theme: theme.id.toString(), + } + } const flagsToPass = this.passThroughFlags(flags, {allowedFlags: Push.cli2Flags}) const command = ['theme', 'push', flags.path, ...flagsToPass] - const store = ensureThemeStore(flags) - const adminSession = await ensureAuthenticatedThemes(store, flags.password) await execCLI2(command, {adminSession}) } } diff --git a/packages/theme/src/cli/services/conf.ts b/packages/theme/src/cli/services/conf.ts index b2419dd1068..97e34ff7e13 100644 --- a/packages/theme/src/cli/services/conf.ts +++ b/packages/theme/src/cli/services/conf.ts @@ -1,10 +1,34 @@ import {Conf} from '@shopify/cli-kit/node/conf' +import {outputDebug, outputContent} from '@shopify/cli-kit/node/output' + +type DevelopmentThemeId = string export interface ThemeConfSchema { themeStore: string } -let _instance: Conf | undefined +interface DevelopmentThemeConfSchema { + [themeStore: string]: DevelopmentThemeId +} + +let _themeConfInstance: Conf | undefined +let _developmentThemeConfInstance: Conf | undefined + +export function themeConf() { + if (!_themeConfInstance) { + _themeConfInstance = new Conf({projectName: 'shopify-cli-theme-conf'}) + } + return _themeConfInstance +} + +export function developmentThemeConf() { + if (!_developmentThemeConfInstance) { + _developmentThemeConfInstance = new Conf({ + projectName: 'shopify-cli-development-theme-conf', + }) + } + return _developmentThemeConfInstance +} export function getThemeStore() { return themeConf().get('themeStore') @@ -14,9 +38,17 @@ export function setThemeStore(store: string) { themeConf().set('themeStore', store) } -function themeConf() { - if (!_instance) { - _instance = new Conf({projectName: 'shopify-cli-theme-conf'}) - } - return _instance +export function getDevelopmentTheme(): string | undefined { + outputDebug(outputContent`Getting development theme...`) + return developmentThemeConf().get(getThemeStore()) +} + +export function setDevelopmentTheme(theme: string): void { + outputDebug(outputContent`Setting development theme...`) + developmentThemeConf().set(getThemeStore(), theme) +} + +export function removeDevelopmentTheme(): void { + outputDebug(outputContent`Removing development theme...`) + developmentThemeConf().reset(getThemeStore()) } diff --git a/packages/theme/src/cli/services/delete.test.ts b/packages/theme/src/cli/services/delete.test.ts index 1766091708f..e25f72699d6 100644 --- a/packages/theme/src/cli/services/delete.test.ts +++ b/packages/theme/src/cli/services/delete.test.ts @@ -1,13 +1,18 @@ import {deleteThemes, renderDeprecatedArgsWarning} from './delete.js' -import {Theme} from '../models/theme.js' -import {deleteTheme} from '../utilities/themes-api.js' import {findOrSelectTheme, findThemes} from '../utilities/theme-selector.js' +import {deleteTheme} from '@shopify/cli-kit/node/themes/themes-api' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' import {test, describe, expect, vi} from 'vitest' import {renderConfirmationPrompt, renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui' vi.mock('@shopify/cli-kit/node/ui') -vi.mock('../utilities/themes-api.js') +vi.mock('@shopify/cli-kit/node/themes/themes-api') vi.mock('../utilities/theme-selector.js') +vi.mock('../utilities/development-theme-manager.js', () => { + const DevelopmentThemeManager = vi.fn() + DevelopmentThemeManager.prototype.find = () => theme1 + return {DevelopmentThemeManager} +}) const session = { token: 'token', diff --git a/packages/theme/src/cli/services/delete.ts b/packages/theme/src/cli/services/delete.ts index b9f87b326b9..6ce4755f8a1 100644 --- a/packages/theme/src/cli/services/delete.ts +++ b/packages/theme/src/cli/services/delete.ts @@ -1,7 +1,9 @@ +import {removeDevelopmentTheme} from './conf.js' import {findOrSelectTheme, findThemes} from '../utilities/theme-selector.js' -import {Theme} from '../models/theme.js' import {themeComponent, themesComponent} from '../utilities/theme-ui.js' -import {deleteTheme} from '../utilities/themes-api.js' +import {DevelopmentThemeManager} from '../utilities/development-theme-manager.js' +import {deleteTheme} from '@shopify/cli-kit/node/themes/themes-api' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' import {AdminSession} from '@shopify/cli-kit/node/session' import { renderConfirmationPrompt, @@ -21,14 +23,25 @@ export interface DeleteOptions { } export async function deleteThemes(adminSession: AdminSession, options: DeleteOptions) { + let themeIds = options.themes + if (options.development) { + const theme = await new DevelopmentThemeManager(adminSession).find() + themeIds = [theme.id.toString()] + } + const store = adminSession.storeFqdn - const themes = await findThemesByDeleteOptions(adminSession, options) + const themes = await findThemesByDeleteOptions(adminSession, {...options, themes: themeIds, development: false}) if (!options.force && !(await isConfirmed(themes, store))) { return } - themes.map((theme) => deleteTheme(theme.id, adminSession)) + themes.map((theme) => { + if (theme.hasDevelopmentRole) { + removeDevelopmentTheme() + } + return deleteTheme(theme.id, adminSession) + }) renderSuccess({ body: pluralize( @@ -40,7 +53,7 @@ export async function deleteThemes(adminSession: AdminSession, options: DeleteOp } async function findThemesByDeleteOptions(adminSession: AdminSession, options: DeleteOptions) { - const isSingleThemeSelection = options.selectTheme || options.development || options.themes.length <= 1 + const isSingleThemeSelection = options.selectTheme || options.themes.length <= 1 if (!isSingleThemeSelection) { return findThemes(adminSession, options) diff --git a/packages/theme/src/cli/services/info.ts b/packages/theme/src/cli/services/info.ts index b27fe04e3fd..4ba0d79bfae 100644 --- a/packages/theme/src/cli/services/info.ts +++ b/packages/theme/src/cli/services/info.ts @@ -1,4 +1,4 @@ -import {getThemeStore} from './conf.js' +import {getDevelopmentTheme, getThemeStore} from './conf.js' import {platformAndArch} from '@shopify/cli-kit/node/os' import {version as rubyVersion} from '@shopify/cli-kit/node/ruby' import {checkForNewVersion} from '@shopify/cli-kit/node/node-package-manager' @@ -14,7 +14,12 @@ export async function themeInfo(config: {cliVersion: string}): Promise { it('should call the table render function, with correctly formatted data', async () => { + const developmentThemeId = 5 vi.mocked(fetchStoreThemes).mockResolvedValue([ - {id: 1361, name: 'Dawn', role: 'live'}, - {id: 1363, name: 'Studio', role: ''}, + {id: 1, name: 'Theme 1', role: 'live'}, + {id: 2, name: 'Theme 2', role: ''}, + {id: 3, name: 'Theme 3', role: 'development'}, + {id: developmentThemeId, name: 'Theme 5', role: 'development'}, ] as Theme[]) + vi.mocked(getDevelopmentTheme).mockReturnValue(developmentThemeId.toString()) await list(session, {}) expect(renderTable).toBeCalledWith({ rows: [ - {id: '#1361', name: 'Dawn', role: '[live]'}, - {id: '#1363', name: 'Studio', role: ''}, + {id: '#1', name: 'Theme 1', role: '[live]'}, + {id: '#2', name: 'Theme 2', role: ''}, + {id: '#3', name: 'Theme 3', role: '[development]'}, + {id: '#5', name: 'Theme 5', role: '[development] [yours]'}, ], columns, }) diff --git a/packages/theme/src/cli/services/list.ts b/packages/theme/src/cli/services/list.ts index 50117c5bc54..ad2befae0de 100644 --- a/packages/theme/src/cli/services/list.ts +++ b/packages/theme/src/cli/services/list.ts @@ -1,4 +1,5 @@ import {columns} from './list.columns.js' +import {getDevelopmentTheme} from './conf.js' import {ALLOWED_ROLES, fetchStoreThemes, Role} from '../utilities/theme-selector/fetch.js' import {Filter, FilterProps, filterThemes} from '../utilities/theme-selector/filter.js' import {renderTable} from '@shopify/cli-kit/node/ui' @@ -21,15 +22,25 @@ export async function list(adminSession: AdminSession, options: Options) { }) let storeThemes = await fetchStoreThemes(adminSession) + const developmentTheme = getDevelopmentTheme() if (filter.any()) { storeThemes = filterThemes(store, storeThemes, filter) } - const themes = storeThemes.map(({id, name, role}) => ({ - id: `#${id}`, - name, - role: role ? `[${role}]` : '', - })) + const themes = storeThemes.map(({id, name, role}) => { + let formattedRole = '' + if (role) { + formattedRole = `[${role}]` + if ([developmentTheme].includes(`${id}`)) { + formattedRole += ' [yours]' + } + } + return { + id: `#${id}`, + name, + role: formattedRole, + } + }) renderTable({rows: themes, columns}) } diff --git a/packages/theme/src/cli/services/open.test.ts b/packages/theme/src/cli/services/open.test.ts index 35aae5de949..c2b17901acb 100644 --- a/packages/theme/src/cli/services/open.test.ts +++ b/packages/theme/src/cli/services/open.test.ts @@ -1,6 +1,6 @@ import {open} from './open.js' import {findOrSelectTheme} from '../utilities/theme-selector.js' -import {Theme} from '../models/theme.js' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' import {test, describe, expect, vi} from 'vitest' import {openURL} from '@shopify/cli-kit/node/system' import {renderInfo} from '@shopify/cli-kit/node/ui' diff --git a/packages/theme/src/cli/services/open.ts b/packages/theme/src/cli/services/open.ts index ea181273f6a..ef1547cf84d 100644 --- a/packages/theme/src/cli/services/open.ts +++ b/packages/theme/src/cli/services/open.ts @@ -1,6 +1,7 @@ import {findOrSelectTheme} from '../utilities/theme-selector.js' -import {themeEditorUrl, themePreviewUrl} from '../utilities/theme-urls.js' import {themeComponent} from '../utilities/theme-ui.js' +import {DevelopmentThemeManager} from '../utilities/development-theme-manager.js' +import {themeEditorUrl, themePreviewUrl} from '@shopify/cli-kit/node/themes/theme-urls' import {openURL} from '@shopify/cli-kit/node/system' import {renderInfo} from '@shopify/cli-kit/node/ui' import {AdminSession} from '@shopify/cli-kit/node/session' @@ -9,12 +10,17 @@ export async function open( adminSession: AdminSession, options: {development: boolean; live: boolean; editor: boolean; theme: string | undefined}, ) { + let themeId = options.theme + if (options.development) { + const theme = await new DevelopmentThemeManager(adminSession).find() + themeId = theme.id.toString() + } + const theme = await findOrSelectTheme(adminSession, { header: 'Select a theme to open', filter: { - development: options.development, live: options.live, - theme: options.theme, + theme: themeId, }, }) diff --git a/packages/theme/src/cli/services/publish.test.ts b/packages/theme/src/cli/services/publish.test.ts index 756194f23b8..373231d5b67 100644 --- a/packages/theme/src/cli/services/publish.test.ts +++ b/packages/theme/src/cli/services/publish.test.ts @@ -1,14 +1,14 @@ import {publish} from './publish.js' import {findOrSelectTheme} from '../utilities/theme-selector.js' -import {Theme} from '../models/theme.js' -import {publishTheme} from '../utilities/themes-api.js' +import {publishTheme} from '@shopify/cli-kit/node/themes/themes-api' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' import {renderSuccess, renderConfirmationPrompt} from '@shopify/cli-kit/node/ui' import {test, describe, expect, vi} from 'vitest' vi.mock('@shopify/cli-kit/node/system') vi.mock('@shopify/cli-kit/node/ui') +vi.mock('@shopify/cli-kit/node/themes/themes-api') vi.mock('../utilities/theme-selector.js') -vi.mock('../utilities/themes-api') const session = { token: 'token', diff --git a/packages/theme/src/cli/services/publish.ts b/packages/theme/src/cli/services/publish.ts index 980a583dedf..9807fd5d365 100644 --- a/packages/theme/src/cli/services/publish.ts +++ b/packages/theme/src/cli/services/publish.ts @@ -1,8 +1,8 @@ -import {Theme} from '../models/theme.js' import {findOrSelectTheme} from '../utilities/theme-selector.js' -import {themePreviewUrl} from '../utilities/theme-urls.js' -import {publishTheme} from '../utilities/themes-api.js' import {themeComponent} from '../utilities/theme-ui.js' +import {publishTheme} from '@shopify/cli-kit/node/themes/themes-api' +import {themePreviewUrl} from '@shopify/cli-kit/node/themes/theme-urls' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' import {renderConfirmationPrompt, renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui' import {AdminSession} from '@shopify/cli-kit/node/session' diff --git a/packages/theme/src/cli/utilities/development-theme-manager.test.ts b/packages/theme/src/cli/utilities/development-theme-manager.test.ts new file mode 100644 index 00000000000..f0f8a90fec4 --- /dev/null +++ b/packages/theme/src/cli/utilities/development-theme-manager.test.ts @@ -0,0 +1,85 @@ +import { + DevelopmentThemeManager, + NO_DEVELOPMENT_THEME_ID_SET, + DEVELOPMENT_THEME_NOT_FOUND, +} from './development-theme-manager.js' +import {getDevelopmentTheme, setDevelopmentTheme, removeDevelopmentTheme} from '../services/conf.js' +import {createTheme, fetchTheme} from '@shopify/cli-kit/node/themes/themes-api' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' +import {beforeEach, describe, expect, it, vi} from 'vitest' + +vi.mock('@shopify/cli-kit/node/themes/themes-api') +vi.mock('../services/conf.js') + +describe('DevelopmentThemeManager', () => { + const storeFqdn = 'mystore.myshopify.com' + const token = 'token' + const existingId = 200 + const newThemeId = 201 + const onlyLocallyExistingId = 404 + const themeTestDatabase: {[id: number]: Theme | undefined} = { + [existingId]: {id: existingId} as Theme, + [onlyLocallyExistingId]: undefined, + } + let localDevelopmentThemeId: string | undefined + + beforeEach(() => { + vi.mocked(getDevelopmentTheme).mockImplementation(() => localDevelopmentThemeId) + vi.mocked(setDevelopmentTheme).mockImplementation(() => undefined) + vi.mocked(removeDevelopmentTheme).mockImplementation(() => undefined) + + vi.mocked(fetchTheme).mockImplementation((id: number) => Promise.resolve(themeTestDatabase[id])) + vi.mocked(createTheme).mockImplementation(({name, role}) => + Promise.resolve(new Theme(newThemeId, name as string, role as string)), + ) + }) + + function buildDevelopmentThemeManager() { + return new DevelopmentThemeManager({ + storeFqdn, + token, + }) + } + + describe('find', () => { + it('should throw Abort if no ID is locally stored', async () => { + localDevelopmentThemeId = undefined + await expect(() => buildDevelopmentThemeManager().find()).rejects.toThrowError(NO_DEVELOPMENT_THEME_ID_SET) + expect(removeDevelopmentTheme).not.toHaveBeenCalled() + }) + + it('should remove locally stored ID and throw Abort if API could not return theme', async () => { + const theme = onlyLocallyExistingId.toString() + localDevelopmentThemeId = theme + await expect(() => buildDevelopmentThemeManager().find()).rejects.toThrowError(DEVELOPMENT_THEME_NOT_FOUND(theme)) + expect(removeDevelopmentTheme).toHaveBeenCalledOnce() + }) + + it('should return theme if API returns theme with locally stored ID', async () => { + const theme = existingId.toString() + localDevelopmentThemeId = theme + expect(await buildDevelopmentThemeManager().find()).toEqual(themeTestDatabase[existingId]) + }) + }) + + describe('findOrCreate', () => { + it('should not create a new development theme if API returns theme with locally stored ID', async () => { + const theme = existingId.toString() + localDevelopmentThemeId = theme + expect((await buildDevelopmentThemeManager().findOrCreate()).id.toString()).toEqual(theme) + }) + + it('should create a new development theme if no ID is locally stored', async () => { + localDevelopmentThemeId = undefined + expect((await buildDevelopmentThemeManager().findOrCreate()).id.toString()).toEqual(newThemeId.toString()) + expect(removeDevelopmentTheme).not.toHaveBeenCalled() + }) + + it('should create a new development theme if locally existing ID points to nowhere', async () => { + const theme = onlyLocallyExistingId.toString() + localDevelopmentThemeId = theme + expect((await buildDevelopmentThemeManager().findOrCreate()).id.toString()).toEqual(newThemeId.toString()) + expect(removeDevelopmentTheme).toHaveBeenCalledOnce() + }) + }) +}) diff --git a/packages/theme/src/cli/utilities/development-theme-manager.ts b/packages/theme/src/cli/utilities/development-theme-manager.ts new file mode 100644 index 00000000000..d704e23a281 --- /dev/null +++ b/packages/theme/src/cli/utilities/development-theme-manager.ts @@ -0,0 +1,35 @@ +import {getDevelopmentTheme, setDevelopmentTheme, removeDevelopmentTheme} from '../services/conf.js' +import {ThemeManager} from '@shopify/cli-kit/node/themes/theme-manager' +import {AdminSession} from '@shopify/cli-kit/node/session' +import {AbortError} from '@shopify/cli-kit/node/error' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' + +export const DEVELOPMENT_THEME_NOT_FOUND = (themeId: string) => + `Development theme #${themeId} could not be found. Please create a new development theme.` +export const NO_DEVELOPMENT_THEME_ID_SET = + 'No development theme ID has been set. Please create a development theme first.' + +export class DevelopmentThemeManager extends ThemeManager { + protected context = 'Development' + + constructor(adminSession: AdminSession) { + super(adminSession) + this.themeId = getDevelopmentTheme() + } + + async find(): Promise { + const theme = await this.fetch() + if (!theme) { + throw new AbortError(this.themeId ? DEVELOPMENT_THEME_NOT_FOUND(this.themeId) : NO_DEVELOPMENT_THEME_ID_SET) + } + return theme + } + + protected setTheme(themeId: string): void { + setDevelopmentTheme(themeId) + } + + protected removeTheme(): void { + removeDevelopmentTheme() + } +} diff --git a/packages/theme/src/cli/utilities/theme-selector.test.ts b/packages/theme/src/cli/utilities/theme-selector.test.ts index 41e07b3fd36..6f14fab3d9a 100644 --- a/packages/theme/src/cli/utilities/theme-selector.test.ts +++ b/packages/theme/src/cli/utilities/theme-selector.test.ts @@ -1,6 +1,6 @@ import {fetchStoreThemes} from './theme-selector/fetch.js' import {findOrSelectTheme, findThemes} from './theme-selector.js' -import {Theme} from '../models/theme.js' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' import {test, describe, vi, expect} from 'vitest' import {renderSelectPrompt} from '@shopify/cli-kit/node/ui' diff --git a/packages/theme/src/cli/utilities/theme-selector/fetch.test.ts b/packages/theme/src/cli/utilities/theme-selector/fetch.test.ts index 26705dd33b0..3a913645a1f 100644 --- a/packages/theme/src/cli/utilities/theme-selector/fetch.test.ts +++ b/packages/theme/src/cli/utilities/theme-selector/fetch.test.ts @@ -1,12 +1,12 @@ import {fetchStoreThemes} from './fetch.js' -import {fetchThemes} from '../themes-api.js' -import {Theme} from '../../models/theme.js' +import {fetchThemes} from '@shopify/cli-kit/node/themes/themes-api' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' import {test, vi, describe, expect} from 'vitest' import {AbortError} from '@shopify/cli-kit/node/error' const session = {token: 'token', storeFqdn: 'my-shop.myshopify.com'} -vi.mock('../themes-api.js') +vi.mock('@shopify/cli-kit/node/themes/themes-api') describe('fetchStoreThemes', () => { test('returns only allowed themes', async () => { diff --git a/packages/theme/src/cli/utilities/theme-selector/fetch.ts b/packages/theme/src/cli/utilities/theme-selector/fetch.ts index d2845768afb..0642550a539 100644 --- a/packages/theme/src/cli/utilities/theme-selector/fetch.ts +++ b/packages/theme/src/cli/utilities/theme-selector/fetch.ts @@ -1,5 +1,5 @@ -import {fetchThemes} from '../themes-api.js' -import {Theme} from '../../models/theme.js' +import {fetchThemes} from '@shopify/cli-kit/node/themes/themes-api' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' import {AdminSession} from '@shopify/cli-kit/node/session' import {AbortError} from '@shopify/cli-kit/node/error' diff --git a/packages/theme/src/cli/utilities/theme-selector/filter.test.ts b/packages/theme/src/cli/utilities/theme-selector/filter.test.ts index ae4b4e1440a..acf0f8f1e0f 100644 --- a/packages/theme/src/cli/utilities/theme-selector/filter.test.ts +++ b/packages/theme/src/cli/utilities/theme-selector/filter.test.ts @@ -1,5 +1,5 @@ import {Filter, filterThemes} from './filter.js' -import {Theme} from '../../models/theme.js' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' import {test, describe, expect} from 'vitest' const store = 'my-shop.myshopify.com' @@ -30,21 +30,7 @@ describe('filterThemes', () => { expect(filtered[0]!.name).toBe('theme (3)') }) - test('filters the development theme', async () => { - /** - * TODO: Return _your_ development theme. - * - * CLI2 creates the development theme and persists the ID - * on a PStore file. Thus, only the CLI2 can differentiate - * between _your_ development theme and the others. - * - * Currently, this filter just returns all development - * themes, but it should only return _yours_. - * - * As soon as the development gets created at the CLI3 level - * this issue must be fixed. - */ - + test('filters by role', async () => { // Given const filter = new Filter({ development: true, diff --git a/packages/theme/src/cli/utilities/theme-selector/filter.ts b/packages/theme/src/cli/utilities/theme-selector/filter.ts index fa7ac4fba28..ba2c1ed7f70 100644 --- a/packages/theme/src/cli/utilities/theme-selector/filter.ts +++ b/packages/theme/src/cli/utilities/theme-selector/filter.ts @@ -1,5 +1,5 @@ import {ALLOWED_ROLES} from './fetch.js' -import {Theme} from '../../models/theme.js' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' import {AbortError} from '@shopify/cli-kit/node/error' export function filterThemes(store: string, themes: Theme[], filter: Filter): Theme[] { diff --git a/packages/theme/src/cli/utilities/theme-ui.test.ts b/packages/theme/src/cli/utilities/theme-ui.test.ts index f8afe2a4e47..c32268100dd 100644 --- a/packages/theme/src/cli/utilities/theme-ui.test.ts +++ b/packages/theme/src/cli/utilities/theme-ui.test.ts @@ -1,5 +1,5 @@ import {themeComponent, themesComponent} from './theme-ui.js' -import {Theme} from '../models/theme.js' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' import {test, describe, expect} from 'vitest' describe('themeComponent', () => { diff --git a/packages/theme/src/cli/utilities/theme-ui.ts b/packages/theme/src/cli/utilities/theme-ui.ts index ef1ce3bad7e..0ee3e46952b 100644 --- a/packages/theme/src/cli/utilities/theme-ui.ts +++ b/packages/theme/src/cli/utilities/theme-ui.ts @@ -1,4 +1,4 @@ -import {Theme} from '../models/theme.js' +import {Theme} from '@shopify/cli-kit/node/themes/models/theme' export function themeComponent(theme: Theme) { return [