From 286ca7fe4379bb94081d180cb0246203d3dd6259 Mon Sep 17 00:00:00 2001 From: Elana Kopelevich Date: Wed, 21 Jan 2026 12:13:02 -0700 Subject: [PATCH 1/5] WIP: asset pipeline for hosted static app --- .../cli/models/extensions/extension-instance.ts | 6 ++++++ .../src/cli/models/extensions/specification.ts | 5 ++++- .../specifications/app_config_app_home.ts | 16 ++++++++++++++++ .../services/dev/app-events/app-event-watcher.ts | 8 ++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/app/src/cli/models/extensions/extension-instance.ts b/packages/app/src/cli/models/extensions/extension-instance.ts index 3a37dd9aaf8..ffeb76080b2 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.ts @@ -366,7 +366,13 @@ export class ExtensionInstance(spec: { identifier: string schema: ZodSchemaType + buildConfig?: BuildConfig appModuleFeatures?: (config?: TConfiguration) => ExtensionFeature[] transformConfig: TransformationConfig | CustomTransformationConfig uidStrategy?: UidStrategy getDevSessionUpdateMessages?: (config: TConfiguration) => Promise patchWithAppDevURLs?: (config: TConfiguration, urls: ApplicationURLs) => void + copyStaticAssets?: (config: TConfiguration, directory: string, outputPath: string) => Promise }): ExtensionSpecification { const appModuleFeatures = spec.appModuleFeatures ?? (() => []) return createExtensionSpecification({ @@ -258,6 +260,7 @@ export function createConfigExtensionSpecification { @@ -31,6 +36,17 @@ const appHomeSpec = createConfigExtensionSpecification({ getDevSessionUpdateMessages: async (config) => { return [`Using URL: ${config.application_url}`] }, + copyStaticAssets: async (config, directory, outputPath) => { + console.log({config}) + config.static_root = 'hosted-app/dist' + if (!config.static_root) return + const sourceDir = joinPath(directory, config.static_root) + const outputDir = dirname(outputPath) + + return copyDirectoryContents(sourceDir, outputDir).catch((error) => { + throw new Error(`Failed to copy static assets from ${sourceDir} to ${outputDir}: ${error.message}`) + }) + }, }) export default appHomeSpec diff --git a/packages/app/src/cli/services/dev/app-events/app-event-watcher.ts b/packages/app/src/cli/services/dev/app-events/app-event-watcher.ts index 5a760de6332..313869f0cc6 100644 --- a/packages/app/src/cli/services/dev/app-events/app-event-watcher.ts +++ b/packages/app/src/cli/services/dev/app-events/app-event-watcher.ts @@ -148,6 +148,7 @@ export class AppEventWatcher extends EventEmitter { this.fileWatcher.onChange((events) => { handleWatcherEvents(events, this.app, this.options) .then(async (appEvent) => { + console.log({appEvent}) if (appEvent?.extensionEvents.length === 0) outputDebug('Change detected, but no extensions were affected') if (!appEvent) return @@ -169,6 +170,13 @@ export class AppEventWatcher extends EventEmitter { await this.app.generateExtensionTypes() } + if (appEvent.appWasReloaded) { + const appHomeExtension = this.app.realExtensions.find((ext) => ext.specification.identifier === 'app_home') + if (appHomeExtension) { + await appHomeExtension.copyStaticAssets(this.buildOutputPath) + } + } + // Find deleted extensions and delete their previous build output await this.deleteExtensionsBuildOutput(appEvent) this.emit('all', appEvent) From dffc8036b68ce6bab58f543d7dcf344028093127 Mon Sep 17 00:00:00 2001 From: Elana Kopelevich Date: Wed, 28 Jan 2026 16:11:14 -0700 Subject: [PATCH 2/5] Add logs for config --- packages/app/src/cli/models/app/loader.ts | 4 +++- .../models/extensions/specifications/app_config_app_home.ts | 2 +- .../cli/services/generate/fetch-extension-specifications.ts | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/app/src/cli/models/app/loader.ts b/packages/app/src/cli/models/app/loader.ts index d83e575846c..8b0dba00bf9 100644 --- a/packages/app/src/cli/models/app/loader.ts +++ b/packages/app/src/cli/models/app/loader.ts @@ -90,6 +90,7 @@ export async function loadConfigurationFileContent( try { const configurationContent = await readFile(filepath) + console.log('Initial undecoded config from TOML file', {configurationContent}) return decode(configurationContent) // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { @@ -140,6 +141,7 @@ export function parseConfigurationObject( const fallbackOutput = {} as zod.TypeOf const parseResult = schema.safeParse(configurationObject) + console.log('Config after comparing with schema (includes remote contract)', {parseResult}) if (!parseResult.success) { return abortOrReport( outputContent`\n${outputToken.errorText('Validation errors')} in ${outputToken.path( @@ -346,7 +348,7 @@ export async function loadOpaqueApp(options: { const rawConfig = await loadConfigurationFileContent(configurationPath) const parsed = TemplateConfigSchema.parse(rawConfig) const packageManager = await getPackageManager(appDirectory) - + console.log('WE ARE NOT GETTING HERE', {rawConfig}) return { state: 'loaded-template', rawConfig, diff --git a/packages/app/src/cli/models/extensions/specifications/app_config_app_home.ts b/packages/app/src/cli/models/extensions/specifications/app_config_app_home.ts index 511b6b0c66a..62a05f0f0e2 100644 --- a/packages/app/src/cli/models/extensions/specifications/app_config_app_home.ts +++ b/packages/app/src/cli/models/extensions/specifications/app_config_app_home.ts @@ -37,7 +37,7 @@ const appHomeSpec = createConfigExtensionSpecification({ return [`Using URL: ${config.application_url}`] }, copyStaticAssets: async (config, directory, outputPath) => { - console.log({config}) + console.log('&&&&&&&&&', {config}) config.static_root = 'hosted-app/dist' if (!config.static_root) return const sourceDir = joinPath(directory, config.static_root) diff --git a/packages/app/src/cli/services/generate/fetch-extension-specifications.ts b/packages/app/src/cli/services/generate/fetch-extension-specifications.ts index 37544a1c672..becc10ed779 100644 --- a/packages/app/src/cli/services/generate/fetch-extension-specifications.ts +++ b/packages/app/src/cli/services/generate/fetch-extension-specifications.ts @@ -38,6 +38,9 @@ export async function fetchSpecifications({ const extensionSpecifications: FlattenedRemoteSpecification[] = result .filter((specification) => ['extension', 'configuration'].includes(specification.experience)) .map((spec) => { + if (spec.identifier === 'app_home') { + console.log('App home spec', spec) + } const newSpec = spec as FlattenedRemoteSpecification // WORKAROUND: The identifiers in the API are different for these extensions to the ones the CLI // has been using so far. This is a workaround to keep the CLI working until the API is updated. @@ -78,7 +81,6 @@ async function mergeLocalAndRemoteSpecs( const merged = {...localSpec, ...remoteSpec, loadedRemoteSpecs: true} as RemoteAwareExtensionSpecification & FlattenedRemoteSpecification - // If configuration is inside an app.toml -- i.e. single UID mode -- we must be able to parse a partial slice. let handleInvalidAdditionalProperties: HandleInvalidAdditionalProperties switch (merged.uidStrategy) { From 42317c9cfd0ee51812f0e32615050d820f35eec3 Mon Sep 17 00:00:00 2001 From: Elana Kopelevich Date: Thu, 29 Jan 2026 10:40:18 -0700 Subject: [PATCH 3/5] re-add hosted_app spec --- .../specifications/app_config_app_home.ts | 16 --------- .../app_config_hosted_app_home.ts | 34 +++++++++++++++++++ .../fetch-extension-specifications.ts | 4 +-- 3 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 packages/app/src/cli/models/extensions/specifications/app_config_hosted_app_home.ts diff --git a/packages/app/src/cli/models/extensions/specifications/app_config_app_home.ts b/packages/app/src/cli/models/extensions/specifications/app_config_app_home.ts index 62a05f0f0e2..e796b4c0d35 100644 --- a/packages/app/src/cli/models/extensions/specifications/app_config_app_home.ts +++ b/packages/app/src/cli/models/extensions/specifications/app_config_app_home.ts @@ -1,8 +1,6 @@ import {validateUrl} from '../../app/validation/common.js' import {BaseSchemaWithoutHandle} from '../schemas.js' import {TransformationConfig, createConfigExtensionSpecification} from '../specification.js' -import {copyDirectoryContents} from '@shopify/cli-kit/node/fs' -import {dirname, joinPath} from '@shopify/cli-kit/node/path' import {zod} from '@shopify/cli-kit/node/schema' const AppHomeSchema = BaseSchemaWithoutHandle.extend({ @@ -13,21 +11,18 @@ const AppHomeSchema = BaseSchemaWithoutHandle.extend({ url: validateUrl(zod.string().max(255, {message: 'String must be less than 255 characters'})), }) .optional(), - static_root: zod.string().optional(), }) const AppHomeTransformConfig: TransformationConfig = { app_url: 'application_url', embedded: 'embedded', preferences_url: 'app_preferences.url', - static_root: 'static_root', } export const AppHomeSpecIdentifier = 'app_home' const appHomeSpec = createConfigExtensionSpecification({ identifier: AppHomeSpecIdentifier, - buildConfig: {mode: 'static_app'} as const, schema: AppHomeSchema, transformConfig: AppHomeTransformConfig, patchWithAppDevURLs: (config, urls) => { @@ -36,17 +31,6 @@ const appHomeSpec = createConfigExtensionSpecification({ getDevSessionUpdateMessages: async (config) => { return [`Using URL: ${config.application_url}`] }, - copyStaticAssets: async (config, directory, outputPath) => { - console.log('&&&&&&&&&', {config}) - config.static_root = 'hosted-app/dist' - if (!config.static_root) return - const sourceDir = joinPath(directory, config.static_root) - const outputDir = dirname(outputPath) - - return copyDirectoryContents(sourceDir, outputDir).catch((error) => { - throw new Error(`Failed to copy static assets from ${sourceDir} to ${outputDir}: ${error.message}`) - }) - }, }) export default appHomeSpec diff --git a/packages/app/src/cli/models/extensions/specifications/app_config_hosted_app_home.ts b/packages/app/src/cli/models/extensions/specifications/app_config_hosted_app_home.ts new file mode 100644 index 00000000000..2f6925458f6 --- /dev/null +++ b/packages/app/src/cli/models/extensions/specifications/app_config_hosted_app_home.ts @@ -0,0 +1,34 @@ +import {BaseSchemaWithoutHandle} from '../schemas.js' +import {TransformationConfig, createConfigExtensionSpecification} from '../specification.js' +import {copyDirectoryContents} from '@shopify/cli-kit/node/fs' +import {dirname, joinPath} from '@shopify/cli-kit/node/path' +import {zod} from '@shopify/cli-kit/node/schema' + +const HostedAppHomeSchema = BaseSchemaWithoutHandle.extend({ + static_root: zod.string().optional(), +}) + +const AppHomeTransformConfig: TransformationConfig = { + static_root: 'static_root', +} + +export const AppHomeSpecIdentifier = 'hosted_app' + +const hostedAppHomeSpec = createConfigExtensionSpecification({ + identifier: AppHomeSpecIdentifier, + // Why isn't this mode setting working? + buildConfig: {mode: 'static_app'} as const, + schema: HostedAppHomeSchema, + transformConfig: AppHomeTransformConfig, + copyStaticAssets: async (config, directory, outputPath) => { + if (!config.static_root) return + const sourceDir = joinPath(directory, config.static_root) + const outputDir = dirname(outputPath) + + return copyDirectoryContents(sourceDir, outputDir).catch((error) => { + throw new Error(`Failed to copy static assets from ${sourceDir} to ${outputDir}: ${error.message}`) + }) + }, +}) + +export default hostedAppHomeSpec diff --git a/packages/app/src/cli/services/generate/fetch-extension-specifications.ts b/packages/app/src/cli/services/generate/fetch-extension-specifications.ts index becc10ed779..2087e73763c 100644 --- a/packages/app/src/cli/services/generate/fetch-extension-specifications.ts +++ b/packages/app/src/cli/services/generate/fetch-extension-specifications.ts @@ -38,8 +38,8 @@ export async function fetchSpecifications({ const extensionSpecifications: FlattenedRemoteSpecification[] = result .filter((specification) => ['extension', 'configuration'].includes(specification.experience)) .map((spec) => { - if (spec.identifier === 'app_home') { - console.log('App home spec', spec) + if (spec.identifier === 'hosted_app') { + console.log('Hosted app home spec', spec) } const newSpec = spec as FlattenedRemoteSpecification // WORKAROUND: The identifiers in the API are different for these extensions to the ones the CLI From 7baa5783162b519fdbc149a3e48d751fbf65fef9 Mon Sep 17 00:00:00 2001 From: Elana Kopelevich Date: Thu, 29 Jan 2026 12:20:35 -0700 Subject: [PATCH 4/5] Fix buildConfig passthrough and clean up some logs --- packages/app/src/cli/models/app/loader.ts | 7 ++++--- .../app/src/cli/models/extensions/extension-instance.ts | 3 --- .../app/src/cli/models/extensions/load-specifications.ts | 3 +++ packages/app/src/cli/models/extensions/specification.ts | 1 + .../specifications/app_config_hosted_app_home.ts | 9 ++++----- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/app/src/cli/models/app/loader.ts b/packages/app/src/cli/models/app/loader.ts index 8b0dba00bf9..8b249a1a960 100644 --- a/packages/app/src/cli/models/app/loader.ts +++ b/packages/app/src/cli/models/app/loader.ts @@ -90,7 +90,7 @@ export async function loadConfigurationFileContent( try { const configurationContent = await readFile(filepath) - console.log('Initial undecoded config from TOML file', {configurationContent}) + // console.log('Initial undecoded config from TOML file', {configurationContent}) return decode(configurationContent) // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { @@ -141,7 +141,7 @@ export function parseConfigurationObject( const fallbackOutput = {} as zod.TypeOf const parseResult = schema.safeParse(configurationObject) - console.log('Config after comparing with schema (includes remote contract)', {parseResult}) + // console.log('Config after comparing with schema (includes remote contract)', {parseResult}) if (!parseResult.success) { return abortOrReport( outputContent`\n${outputToken.errorText('Validation errors')} in ${outputToken.path( @@ -348,7 +348,7 @@ export async function loadOpaqueApp(options: { const rawConfig = await loadConfigurationFileContent(configurationPath) const parsed = TemplateConfigSchema.parse(rawConfig) const packageManager = await getPackageManager(appDirectory) - console.log('WE ARE NOT GETTING HERE', {rawConfig}) + // console.log('WE ARE NOT GETTING HERE', {rawConfig}) return { state: 'loaded-template', rawConfig, @@ -858,6 +858,7 @@ export async function loadAppConfiguration( options: AppConfigurationLoaderConstructorArgs, ): Promise { const specifications = options.specifications ?? (await loadLocalExtensionsSpecifications()) + console.log('SPECIFICATIONS', {specifications}) const state = await getAppConfigurationState(options.directory, options.userProvidedConfigName) const result = await loadAppConfigurationFromState(state, specifications, options.remoteFlags ?? []) await logMetadataFromAppLoadingProcess(result.configurationLoadResultMetadata) diff --git a/packages/app/src/cli/models/extensions/extension-instance.ts b/packages/app/src/cli/models/extensions/extension-instance.ts index ffeb76080b2..7771605253b 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.ts @@ -367,12 +367,9 @@ export class ExtensionInstance { if (!config.static_root) return const sourceDir = joinPath(directory, config.static_root) From 6557375477cd20b42b1937d6d182ab9c80d6bc11 Mon Sep 17 00:00:00 2001 From: Josh White Date: Thu, 29 Jan 2026 16:22:04 -0500 Subject: [PATCH 5/5] add HostedAppSpecIdentifier to CONFIG_EXTENSION_IDs to set experience to configuration --- packages/app/src/cli/models/extensions/extension-instance.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/app/src/cli/models/extensions/extension-instance.ts b/packages/app/src/cli/models/extensions/extension-instance.ts index 7771605253b..170aabb77e5 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.ts @@ -12,6 +12,7 @@ import {PosSpecIdentifier} from './specifications/app_config_point_of_sale.js' import {PrivacyComplianceWebhooksSpecIdentifier} from './specifications/app_config_privacy_compliance_webhooks.js' import {WebhooksSpecIdentifier} from './specifications/app_config_webhook.js' import {WebhookSubscriptionSpecIdentifier} from './specifications/app_config_webhook_subscription.js' +import {HostedAppHomeSpecIdentifier} from './specifications/app_config_hosted_app_home.js' import { ExtensionBuildOptions, buildFunctionExtension, @@ -40,6 +41,7 @@ export const CONFIG_EXTENSION_IDS: string[] = [ AppHomeSpecIdentifier, AppProxySpecIdentifier, BrandingSpecIdentifier, + HostedAppHomeSpecIdentifier, PosSpecIdentifier, PrivacyComplianceWebhooksSpecIdentifier, WebhookSubscriptionSpecIdentifier,