Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Themes] - Shopify Theme Console #4183

Merged
merged 9 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -4255,6 +4255,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 @@ -4292,7 +4301,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')(() =>
debugLogResponseInfo({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 @@ -1611,14 +1611,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 @@ -4759,6 +4759,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 @@ -4785,7 +4793,6 @@
"type": "option"
},
"port": {
"default": "9293",
"description": "Local port to serve authentication service.",
"env": "SHOPIFY_FLAG_PORT",
"hasDynamicHelp": false,
Expand All @@ -4802,6 +4809,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) {
jamesmengo marked this conversation as resolved.
Show resolved Hide resolved
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
Loading