Skip to content

Commit

Permalink
Merge pull request #4183 from Shopify/jmeng/themeconsole
Browse files Browse the repository at this point in the history
[Themes] - Shopify Theme Console
  • Loading branch information
karreiro committed Aug 9, 2024
2 parents d124952 + ddbe9f5 commit 68e905c
Show file tree
Hide file tree
Showing 22 changed files with 1,195 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-yaks-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/theme': minor
---

Release the developer preview for the Theme Console command
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export interface themeconsole {
*/
'-s, --store <value>'?: string

/**
* The password for storefronts with password protection.
* @environment SHOPIFY_FLAG_STORE_PASSWORD
*/
'--store-password <value>'?: string

/**
* The url to be used as context
* @environment SHOPIFY_FLAG_URL
Expand Down
11 changes: 10 additions & 1 deletion docs-shopify.dev/generated/generated_docs_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -4279,6 +4279,15 @@
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_PORT"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/theme-console.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--store-password <value>",
"value": "string",
"description": "The password for storefronts with password protection.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_STORE_PASSWORD"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/theme-console.interface.ts",
"syntaxKind": "PropertySignature",
Expand Down Expand Up @@ -4316,7 +4325,7 @@
"environmentValue": "SHOPIFY_FLAG_STORE"
}
],
"value": "export interface themeconsole {\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Password generated from the Theme Access app.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password <value>'?: string\n\n /**\n * Local port to serve authentication service.\n * @environment SHOPIFY_FLAG_PORT\n */\n '--port <value>'?: 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 <value>'?: string\n\n /**\n * The url to be used as context\n * @environment SHOPIFY_FLAG_URL\n */\n '--url <value>'?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
"value": "export interface themeconsole {\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Password generated from the Theme Access app.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password <value>'?: string\n\n /**\n * Local port to serve authentication service.\n * @environment SHOPIFY_FLAG_PORT\n */\n '--port <value>'?: 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 <value>'?: string\n\n /**\n * The password for storefronts with password protection.\n * @environment SHOPIFY_FLAG_STORE_PASSWORD\n */\n '--store-password <value>'?: string\n\n /**\n * The url to be used as context\n * @environment SHOPIFY_FLAG_URL\n */\n '--url <value>'?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/cli-kit/src/public/node/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function formData(): FormData {
return new FormData()
}

export type Response = ReturnType<typeof nodeFetch>
export type Response = Awaited<ReturnType<typeof nodeFetch>>

/**
* An interface that abstracts way node-fetch. When Node has built-in
Expand All @@ -33,7 +33,7 @@ export type Response = ReturnType<typeof nodeFetch>
* @param init - An object containing any custom settings that you want to apply to the request.
* @returns A promise that resolves with the response.
*/
export async function fetch(url: RequestInfo, init?: RequestInit): Response {
export async function fetch(url: RequestInfo, init?: RequestInit): Promise<Response> {
return runWithTimer('cmd_all_timing_network_ms')(() =>
simpleRequestWithDebugLog({url: url.toString(), request: () => nodeFetch(url, init)}),
)
Expand All @@ -48,7 +48,7 @@ export async function fetch(url: RequestInfo, init?: RequestInit): Response {
* @param init - An object containing any custom settings that you want to apply to the request.
* @returns A promise that resolves with the response.
*/
export async function shopifyFetch(url: RequestInfo, init?: RequestInit): Response {
export async function shopifyFetch(url: RequestInfo, init?: RequestInit): Promise<Response> {
const sanitizedUrl = sanitizeURL(url.toString())
const options: RequestInit = {
...(init ?? {}),
Expand Down
17 changes: 9 additions & 8 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1618,14 +1618,15 @@ USAGE
$ shopify theme console --url /products/classic-leather-jacket
FLAGS
-e, --environment=<value> The environment to apply to the current command.
-s, --store=<value> Store URL. It can be the store prefix (example) or the full myshopify.com URL
(example.myshopify.com, https://example.myshopify.com).
--no-color Disable color output.
--password=<value> Password generated from the Theme Access app.
--port=<value> [default: 9293] Local port to serve authentication service.
--url=<value> [default: /] The url to be used as context
--verbose Increase the verbosity of the output.
-e, --environment=<value> The environment to apply to the current command.
-s, --store=<value> Store URL. It can be the store prefix (example) or the full myshopify.com URL
(example.myshopify.com, https://example.myshopify.com).
--no-color Disable color output.
--password=<value> Password generated from the Theme Access app.
--port=<value> Local port to serve authentication service.
--store-password=<value> The password for storefronts with password protection.
--url=<value> [default: /] The url to be used as context
--verbose Increase the verbosity of the output.
DESCRIPTION
Shopify Liquid REPL (read-eval-print loop) tool
Expand Down
17 changes: 16 additions & 1 deletion packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4796,6 +4796,14 @@
"description": "Starts the Shopify Liquid REPL (read-eval-print loop) tool. This tool provides an interactive terminal interface for evaluating Liquid code and exploring Liquid objects, filters, and tags using real store data.\n\n You can also provide context to the console using a URL, as some Liquid objects are context-specific",
"descriptionWithMarkdown": "Starts the Shopify Liquid REPL (read-eval-print loop) tool. This tool provides an interactive terminal interface for evaluating Liquid code and exploring Liquid objects, filters, and tags using real store data.\n\n You can also provide context to the console using a URL, as some Liquid objects are context-specific",
"flags": {
"dev-preview": {
"allowNo": false,
"description": "Enables the developer preview for the upcoming `theme console` implementation.",
"env": "SHOPIFY_FLAG_BETA",
"hidden": true,
"name": "dev-preview",
"type": "boolean"
},
"environment": {
"char": "e",
"description": "The environment to apply to the current command.",
Expand All @@ -4822,7 +4830,6 @@
"type": "option"
},
"port": {
"default": "9293",
"description": "Local port to serve authentication service.",
"env": "SHOPIFY_FLAG_PORT",
"hasDynamicHelp": false,
Expand All @@ -4839,6 +4846,14 @@
"name": "store",
"type": "option"
},
"store-password": {
"description": "The password for storefronts with password protection.",
"env": "SHOPIFY_FLAG_STORE_PASSWORD",
"hasDynamicHelp": false,
"multiple": false,
"name": "store-password",
"type": "option"
},
"url": {
"default": "/",
"description": "The url to be used as context",
Expand Down
44 changes: 38 additions & 6 deletions packages/theme/src/cli/commands/theme/console.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {themeFlags} from '../../flags.js'
import ThemeCommand from '../../utilities/theme-command.js'
import {ensureThemeStore} from '../../utilities/theme-store.js'
import {ensureReplEnv, initializeRepl} from '../../services/console.js'
import {globalFlags} from '@shopify/cli-kit/node/cli'
import {ensureAuthenticatedStorefront, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session'
import {execCLI2} from '@shopify/cli-kit/node/ruby'
import {renderInfo} from '@shopify/cli-kit/node/ui'
import {renderInfo, renderWarning} from '@shopify/cli-kit/node/ui'
import {Flags} from '@oclif/core'
import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version'
import {outputInfo} from '@shopify/cli-kit/node/output'

export default class Console extends ThemeCommand {
static summary = 'Shopify Liquid REPL (read-eval-print loop) tool'
Expand All @@ -32,21 +34,40 @@ export default class Console extends ThemeCommand {
port: Flags.string({
description: 'Local port to serve authentication service.',
env: 'SHOPIFY_FLAG_PORT',
default: '9293',
}),
'store-password': Flags.string({
description: 'The password for storefronts with password protection.',
env: 'SHOPIFY_FLAG_STORE_PASSWORD',
}),
'dev-preview': Flags.boolean({
hidden: true,
description: 'Enables the developer preview for the upcoming `theme console` implementation.',
env: 'SHOPIFY_FLAG_BETA',
}),
}

async run() {
const {flags} = await this.parse(Console)
const store = ensureThemeStore(flags)
const {password, url, port} = flags
const {url, port, password: themeAccessPassword} = flags
const cliVersion = CLI_KIT_VERSION
const theme = `liquid-console-repl-${cliVersion}`

const adminSession = await ensureAuthenticatedThemes(store, password, [], true)
const storefrontToken = await ensureAuthenticatedStorefront([], password)
const adminSession = await ensureAuthenticatedThemes(store, themeAccessPassword, [], true)
const storefrontToken = await ensureAuthenticatedStorefront([], themeAccessPassword)
const authUrl = `http://localhost:${port}/password`

if (flags['dev-preview']) {
outputInfo('This feature is currently in development and is not ready for use or testing yet.')

if (flags.port) {
renderPortDeprecationWarning()
}
const {themeId, storePassword} = await ensureReplEnv(adminSession, flags['store-password'])
await initializeRepl(adminSession, storefrontToken, themeId, url, storePassword)
return
}

renderInfo({
body: [
'Activate the Shopify Liquid console in',
Expand All @@ -55,10 +76,21 @@ export default class Console extends ThemeCommand {
],
})

return execCLI2(['theme', 'console', '--url', url, '--port', port, '--theme', theme], {
return execCLI2(['theme', 'console', '--url', url, '--port', port ?? '9293', '--theme', theme], {
store,
adminToken: adminSession.token,
storefrontToken,
})
}
}
function renderPortDeprecationWarning() {
renderWarning({
headline: ['The', {command: '--port'}, 'flag has been deprecated.'],
body: [
{command: 'shopify theme console'},
'no longer requires a port to run. The',
{command: '--port'},
'flag is in the process of being deprecated and will be removed soon.',
],
})
}
69 changes: 69 additions & 0 deletions packages/theme/src/cli/services/console.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {ensureReplEnv} from './console.js'
import {
isStorefrontPasswordCorrect,
isStorefrontPasswordProtected,
} from '../utilities/theme-environment/storefront-session.js'
import {ensureValidPassword} from '../utilities/repl/storefront-password-prompt.js'
import {beforeEach, describe, expect, test, vi} from 'vitest'
import {AdminSession} from '@shopify/cli-kit/node/session'

vi.mock('../utilities/theme-environment/storefront-session.js')
vi.mock('../utilities/repl/storefront-password-prompt.js')
vi.mock('../utilities/repl/repl-theme-manager.js', () => {
const REPLThemeManager = vi.fn()
REPLThemeManager.prototype.findOrCreate = () => ({
id: 1,
name: 'theme',
role: 'development',
createdAtRuntime: true,
processing: true,
})
return {REPLThemeManager}
})

describe('ensureReplEnv', () => {
beforeEach(() => {
vi.mocked(ensureValidPassword).mockResolvedValue('testPassword')
})

const adminSession: AdminSession = {storeFqdn: 'test-store.myshopify.com', token: 'token'}

test('should prompt for password when storefront is password protected', async () => {
// Given
vi.mocked(isStorefrontPasswordProtected).mockResolvedValue(true)
vi.mocked(isStorefrontPasswordCorrect).mockResolvedValue(true)

// When
const {storePassword} = await ensureReplEnv(adminSession)

// Then
expect(ensureValidPassword).toHaveBeenCalled()
expect(storePassword).toBe('testPassword')
})

test('should skip prompt and return undefined for password when storefront is not password protected', async () => {
// Given
vi.mocked(isStorefrontPasswordProtected).mockResolvedValue(false)
vi.mocked(isStorefrontPasswordCorrect).mockResolvedValue(true)

// When
const {storePassword} = await ensureReplEnv(adminSession)

// Then
expect(ensureValidPassword).not.toHaveBeenCalled()
expect(storePassword).toBeUndefined()
})

test('should return undefined for storePassword when password is provided but storefront is not password protected', async () => {
// Given
vi.mocked(isStorefrontPasswordProtected).mockResolvedValue(false)
vi.mocked(isStorefrontPasswordCorrect).mockResolvedValue(true)

// When
const {storePassword} = await ensureReplEnv(adminSession, 'testPassword')

// Then
expect(ensureValidPassword).not.toHaveBeenCalled()
expect(storePassword).toBeUndefined()
})
})
44 changes: 44 additions & 0 deletions packages/theme/src/cli/services/console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {isStorefrontPasswordProtected} from '../utilities/theme-environment/storefront-session.js'
import {REPLThemeManager} from '../utilities/repl/repl-theme-manager.js'
import {DevServerSession} from '../utilities/theme-environment/types.js'
import {ensureValidPassword} from '../utilities/repl/storefront-password-prompt.js'
import {replLoop} from '../utilities/repl/repl.js'
import {AdminSession} from '@shopify/cli-kit/node/session'
import {consoleLog} from '@shopify/cli-kit/node/output'

export async function ensureReplEnv(adminSession: AdminSession, storePasswordFlag?: string) {
const themeId = await findOrCreateReplTheme(adminSession)

const storePassword = (await isStorefrontPasswordProtected(adminSession.storeFqdn))
? await ensureValidPassword(storePasswordFlag, adminSession.storeFqdn)
: undefined

return {
themeId,
storePassword,
}
}

async function findOrCreateReplTheme(adminSession: AdminSession): Promise<string> {
const themeManager = new REPLThemeManager(adminSession)
const replTheme = await themeManager.findOrCreate()

return replTheme.id.toString()
}

export async function initializeRepl(
adminSession: AdminSession,
storefrontToken: string,
themeId: string,
url: string,
password: string | undefined,
) {
consoleLog('Welcome to Shopify Liquid console\n(press Ctrl + C to exit)')
const themeSession: DevServerSession = {
...adminSession,
storefrontToken,
storefrontPassword: password,
expiresAt: new Date(),
}
return replLoop(themeSession, themeId, url)
}
25 changes: 25 additions & 0 deletions packages/theme/src/cli/services/local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface DevelopmentThemeLocalStorageSchema {

let _themeLocalStorageInstance: LocalStorage<ThemeLocalStorageSchema> | undefined
let _developmentThemeLocalStorageInstance: LocalStorage<DevelopmentThemeLocalStorageSchema> | undefined
let _replThemeLocalStorageInstance: LocalStorage<DevelopmentThemeLocalStorageSchema> | undefined

function themeLocalStorage() {
if (!_themeLocalStorageInstance) {
Expand All @@ -30,6 +31,15 @@ function developmentThemeLocalStorage() {
return _developmentThemeLocalStorageInstance
}

function replThemeLocalStorage() {
if (!_replThemeLocalStorageInstance) {
_replThemeLocalStorageInstance = new LocalStorage<DevelopmentThemeLocalStorageSchema>({
projectName: 'shopify-cli-repl-theme-config',
})
}
return _replThemeLocalStorageInstance
}

export function getThemeStore() {
return themeLocalStorage().get('themeStore')
}
Expand All @@ -52,3 +62,18 @@ export function removeDevelopmentTheme(): void {
outputDebug(outputContent`Removing development theme...`)
developmentThemeLocalStorage().delete(getThemeStore())
}

export function getREPLTheme(): string | undefined {
outputDebug(outputContent`Getting REPL theme...`)
return replThemeLocalStorage().get(getThemeStore())
}

export function setREPLTheme(theme: string): void {
outputDebug(outputContent`Setting REPL theme to ${theme}...`)
replThemeLocalStorage().set(getThemeStore(), theme)
}

export function removeREPLTheme(): void {
outputDebug(outputContent`Removing REPL theme...`)
replThemeLocalStorage().delete(getThemeStore())
}
Loading

0 comments on commit 68e905c

Please sign in to comment.