Skip to content

Commit

Permalink
Switch theme profile to use DevServer rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
macournoyer committed Jan 8, 2025
1 parent 3c14b89 commit 8fa9f2f
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 38 deletions.
36 changes: 31 additions & 5 deletions packages/theme/src/cli/commands/theme/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {themeFlags} from '../../flags.js'
import ThemeCommand from '../../utilities/theme-command.js'
import {profile} from '../../services/profile.js'
import {ensureThemeStore} from '../../utilities/theme-store.js'
import {findOrSelectTheme} from '../../utilities/theme-selector.js'
import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session'
import {Flags} from '@oclif/core'
import {globalFlags} from '@shopify/cli-kit/node/cli'

Expand All @@ -20,12 +22,19 @@ export default class Profile extends ThemeCommand {
...globalFlags,
store: themeFlags.store,
password: themeFlags.password,
theme: Flags.string({
char: 't',
description: 'Theme ID or name of the remote theme.',
env: 'SHOPIFY_FLAG_THEME_ID',
}),
url: Flags.string({
char: 'u',
description: 'URL to the theme page to profile.',
description: 'The url to be used as context',
env: 'SHOPIFY_FLAG_URL',
required: true,
parse: async (url) => (url.startsWith('/') ? url : `/${url}`),
default: '/',
}),
'store-password': Flags.string({
description: 'The password for storefronts with password protection.',
env: 'SHOPIFY_FLAG_STORE_PASSWORD',
}),
json: Flags.boolean({
char: 'j',
Expand All @@ -37,7 +46,24 @@ export default class Profile extends ThemeCommand {
async run(): Promise<void> {
const {flags} = await this.parse(Profile)
const store = ensureThemeStore(flags)
const {password: themeAccessPassword} = flags

const adminSession = await ensureAuthenticatedThemes(store, themeAccessPassword, [], true)
let filter
if (flags.theme) {
filter = {filter: {theme: flags.theme}}
} else {
filter = {filter: {live: true}}
}
const theme = await findOrSelectTheme(adminSession, filter)

await profile(flags.password, store, flags.url, flags.json)
await profile(
adminSession,
theme.id.toString(),
flags.url,
flags.json,
themeAccessPassword,
flags['store-password'],
)
}
}
36 changes: 26 additions & 10 deletions packages/theme/src/cli/services/profile.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
import {isStorefrontPasswordProtected} from '../utilities/theme-environment/storefront-session.js'
import {ensureValidPassword} from '../utilities/theme-environment/storefront-password-prompt.js'
import {fetchDevServerSession} from '../utilities/theme-environment/dev-server-session.js'
import {render} from '../utilities/theme-environment/storefront-renderer.js'
import {openURL} from '@shopify/cli-kit/node/system'
import {ensureAuthenticatedStorefront} from '@shopify/cli-kit/node/session'
import {joinPath} from '@shopify/cli-kit/node/path'
import {AdminSession} from '@shopify/cli-kit/node/session'
import {writeFile} from 'fs/promises'
import {tmpdir} from 'os'

export async function profile(password: string | undefined, storeDomain: string, urlPath: string, asJson: boolean) {
// Fetch the profiling from the Store
const url = new URL(`https://${storeDomain}${urlPath}`)
const storefrontToken = await ensureAuthenticatedStorefront([], password)
const response = await fetch(url, {
export async function profile(
adminSession: AdminSession,
themeId: string,
url: string,
asJson: boolean,
themeAccessPassword?: string,
storefrontPassword?: string,
) {
const storePassword = (await isStorefrontPasswordProtected(adminSession.storeFqdn))
? await ensureValidPassword(storefrontPassword, adminSession.storeFqdn)
: undefined

const session = await fetchDevServerSession(themeId, adminSession, themeAccessPassword, storePassword)
const response = await render(session, {
method: 'GET',
path: url,
query: [],
themeId,
headers: {
Authorization: `Bearer ${storefrontToken}`,
Accept: 'application/vnd.speedscope+json',
},
})
const contentType = response.headers.get('content-type')
if (response.status !== 200 || contentType !== 'application/json') {

if (response.status !== 200) {
const body = await response.text()
throw new Error(`Bad response: ${response.status} (content-type: ${contentType}): ${body}`)
throw new Error(`Bad response: ${response.status}: ${body}`)
}

const profileJson = await response.text()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,17 @@ export async function initializeDevServerSession(
return session
}

async function fetchDevServerSession(
/**
* Initialize the session object, without automatic refresh.
*
* @param themeId - The theme being rendered in this session.
* @param adminSession - Admin session with the initial access token and store.
* @param adminPassword - Custom app password or password generated by the Theme Access app.
* @param storefrontPassword - Storefront password set in password-protected stores.
*
* @returns Details about the app configuration state.
*/
export async function fetchDevServerSession(
themeId: string,
adminSession: AdminSession,
adminPassword?: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,42 @@ import {createError} from 'h3'

export async function render(session: DevServerSession, context: DevServerRenderContext): Promise<Response> {
const url = buildStorefrontUrl(session, context)
const replaceTemplates = Object.keys({...context.replaceTemplates, ...context.replaceExtensionTemplates})

outputDebug(`→ Rendering ${url} (with ${replaceTemplates})...`)

const bodyParams = storefrontReplaceTemplatesParams(context)
const headers = await buildHeaders(session, context)

const response = await fetch(url, {
method: 'POST',
body: bodyParams,
headers: {
...headers,
...defaultHeaders(),
},
}).catch((error: Error) => {
throw createError({
status: 502,
statusText: 'Bad Gateway',
data: {url},
cause: error,
let response

if (context.replaceTemplates) {
const replaceTemplates = Object.keys({...context.replaceTemplates, ...context.replaceExtensionTemplates})

outputDebug(`→ Rendering ${url} (with ${replaceTemplates})...`)

const bodyParams = storefrontReplaceTemplatesParams(context)

response = await fetch(url, {
method: 'POST',
body: bodyParams,
headers: {
...headers,
...defaultHeaders(),
},
}).catch((error: Error) => {
throw createError({
status: 502,
statusText: 'Bad Gateway',
data: {url},
cause: error,
})
})
})
} else {
outputDebug(`→ Rendering ${url}...`)

response = await fetch(url, {
method: context.method,
headers: {
...headers,
...defaultHeaders(),
},
})
}

const requestId = response.headers.get('x-request-id')
outputDebug(`← ${response.status} (request_id: ${requestId})`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export function storefrontReplaceTemplatesParams(context: DevServerRenderContext
*/
const params = new URLSearchParams()

for (const [path, content] of Object.entries(context.replaceTemplates)) {
for (const [path, content] of Object.entries(context.replaceTemplates ?? [])) {
params.append(`replace_templates[${path}]`, content)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export interface DevServerRenderContext {
/**
* Custom content to be replaced in the theme during rendering.
*/
replaceTemplates: {[key: string]: string}
replaceTemplates?: {[key: string]: string}

/**
* Custom content to be replaced during rendering.
Expand Down

0 comments on commit 8fa9f2f

Please sign in to comment.