From 6d80d2cceda97be2a0f16d771a048782df737b7a Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Wed, 25 Mar 2026 15:30:53 +0100 Subject: [PATCH] Switch app init template to hosted app when HOSTED_APPS env var is set When HOSTED_APPS=1, the "Build an extension-only app" option in shopify app init is replaced with a hosted app label and points to the shopify-app-template-extension-only repo. Adds isHostedAppsMode() helper to @shopify/cli-kit/node/context/local following the same pattern as isVerbose, isDevelopment, etc. Co-authored-by: Richard Powell Co-authored-by: Claude Sonnet 4.6 --- .../app/src/cli/prompts/init/init.test.ts | 30 ++++++++++++++++++- packages/app/src/cli/prompts/init/init.ts | 22 ++++++++++---- .../cli-kit/src/private/node/constants.ts | 1 + .../src/public/node/context/local.test.ts | 25 ++++++++++++++++ .../cli-kit/src/public/node/context/local.ts | 10 +++++++ 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/packages/app/src/cli/prompts/init/init.test.ts b/packages/app/src/cli/prompts/init/init.test.ts index fa4c5bdfbfd..bba278daa75 100644 --- a/packages/app/src/cli/prompts/init/init.test.ts +++ b/packages/app/src/cli/prompts/init/init.test.ts @@ -1,10 +1,12 @@ -import init, {InitOptions} from './init.js' +import init, {buildNoneTemplate, InitOptions} from './init.js' import {describe, expect, vi, test, beforeEach} from 'vitest' import {renderSelectPrompt} from '@shopify/cli-kit/node/ui' import {installGlobalCLIPrompt} from '@shopify/cli-kit/node/is-global' +import {isHostedAppsMode} from '@shopify/cli-kit/node/context/local' vi.mock('@shopify/cli-kit/node/ui') vi.mock('@shopify/cli-kit/node/is-global') +vi.mock('@shopify/cli-kit/node/context/local') const globalCLIResult = {install: true, alreadyInstalled: false} @@ -37,6 +39,32 @@ describe('init', () => { expect(got).toEqual({...options, ...answers, templateType: 'none', globalCLIResult}) }) + describe('buildNoneTemplate', () => { + test('returns hosted app label and URL when HOSTED_APPS is enabled', () => { + // Given + vi.mocked(isHostedAppsMode).mockReturnValue(true) + + // When + const got = buildNoneTemplate() + + // Then + expect(got.label).toBe('Build an extension-only app (Shopify-hosted Preact app home and extensions, no back-end)') + expect(got.url).toBe('https://github.com/Shopify/shopify-app-template-extension-only') + }) + + test('returns default label and URL when HOSTED_APPS is not set', () => { + // Given + vi.mocked(isHostedAppsMode).mockReturnValue(false) + + // When + const got = buildNoneTemplate() + + // Then + expect(got.label).toBe('Build an extension-only app') + expect(got.url).toBe('https://github.com/Shopify/shopify-app-template-none') + }) + }) + test('it renders branches for templates that have them', async () => { const answers = { template: 'https://github.com/Shopify/shopify-app-template-react-router#javascript-cli', diff --git a/packages/app/src/cli/prompts/init/init.ts b/packages/app/src/cli/prompts/init/init.ts index 7f1174fb482..0e5f556c19f 100644 --- a/packages/app/src/cli/prompts/init/init.ts +++ b/packages/app/src/cli/prompts/init/init.ts @@ -1,4 +1,5 @@ import {InstallGlobalCLIPromptResult, installGlobalCLIPrompt} from '@shopify/cli-kit/node/is-global' +import {isHostedAppsMode} from '@shopify/cli-kit/node/context/local' import {renderSelectPrompt} from '@shopify/cli-kit/node/ui' export interface InitOptions { @@ -28,6 +29,19 @@ interface Template { } } +export function buildNoneTemplate(): Template { + const hostedAppsEnabled = isHostedAppsMode() + return { + url: hostedAppsEnabled + ? 'https://github.com/Shopify/shopify-app-template-extension-only' + : 'https://github.com/Shopify/shopify-app-template-none', + label: hostedAppsEnabled + ? 'Build an extension-only app (Shopify-hosted Preact app home and extensions, no back-end)' + : 'Build an extension-only app', + visible: true, + } +} + // Eventually this list should be taken from a remote location // That way we don't have to update the CLI every time we add a template export const templates = { @@ -55,11 +69,7 @@ export const templates = { }, }, } as Template, - none: { - url: 'https://github.com/Shopify/shopify-app-template-none', - label: 'Build an extension-only app', - visible: true, - } as Template, + none: buildNoneTemplate(), node: { url: 'https://github.com/Shopify/shopify-app-template-node', visible: false, @@ -68,7 +78,7 @@ export const templates = { url: 'https://github.com/Shopify/shopify-app-template-ruby', visible: false, } as Template, -} as const +} type PredefinedTemplate = keyof typeof templates const allTemplates = Object.keys(templates) as Readonly diff --git a/packages/cli-kit/src/private/node/constants.ts b/packages/cli-kit/src/private/node/constants.ts index 4a6a94f0beb..5cc85764df3 100644 --- a/packages/cli-kit/src/private/node/constants.ts +++ b/packages/cli-kit/src/private/node/constants.ts @@ -47,6 +47,7 @@ export const environmentVariables = { neverUsePartnersApi: 'SHOPIFY_CLI_NEVER_USE_PARTNERS_API', skipNetworkLevelRetry: 'SHOPIFY_CLI_SKIP_NETWORK_LEVEL_RETRY', maxRequestTimeForNetworkCalls: 'SHOPIFY_CLI_MAX_REQUEST_TIME_FOR_NETWORK_CALLS', + hostedApps: 'HOSTED_APPS', } export const defaultThemeKitAccessDomain = 'theme-kit-access.shopifyapps.com' diff --git a/packages/cli-kit/src/public/node/context/local.test.ts b/packages/cli-kit/src/public/node/context/local.test.ts index 10c4a37f07c..abd5c523a6a 100644 --- a/packages/cli-kit/src/public/node/context/local.test.ts +++ b/packages/cli-kit/src/public/node/context/local.test.ts @@ -2,6 +2,7 @@ import { ciPlatform, hasGit, isDevelopment, + isHostedAppsMode, isShopify, isUnitTest, analyticsDisabled, @@ -45,6 +46,30 @@ describe('isDevelopment', () => { }) }) +describe('isHostedAppsMode', () => { + test('returns true when HOSTED_APPS is truthy', () => { + // Given + const env = {HOSTED_APPS: '1'} + + // When + const got = isHostedAppsMode(env) + + // Then + expect(got).toBe(true) + }) + + test('returns false when HOSTED_APPS is not set', () => { + // Given + const env = {} + + // When + const got = isHostedAppsMode(env) + + // Then + expect(got).toBe(false) + }) +}) + describe('isShopify', () => { test('returns false when the SHOPIFY_RUN_AS_USER env. variable is truthy', async () => { // Given diff --git a/packages/cli-kit/src/public/node/context/local.ts b/packages/cli-kit/src/public/node/context/local.ts index c8910841442..116ff50257a 100644 --- a/packages/cli-kit/src/public/node/context/local.ts +++ b/packages/cli-kit/src/public/node/context/local.ts @@ -47,6 +47,16 @@ export function isVerbose(env = process.env): boolean { return isTruthy(env[environmentVariables.verbose]) || process.argv.includes('--verbose') } +/** + * Returns true if the hosted apps mode is enabled. + * + * @param env - The environment variables from the environment of the current process. + * @returns True if HOSTED_APPS is truthy. + */ +export function isHostedAppsMode(env = process.env): boolean { + return isTruthy(env[environmentVariables.hostedApps]) +} + /** * Returns true if the environment in which the CLI is running is either * a local environment (where dev is present).