Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -40,6 +41,7 @@ export const CONFIG_EXTENSION_IDS: string[] = [
AppHomeSpecIdentifier,
AppProxySpecIdentifier,
BrandingSpecIdentifier,
HostedAppHomeSpecIdentifier,
PosSpecIdentifier,
PrivacyComplianceWebhooksSpecIdentifier,
WebhookSubscriptionSpecIdentifier,
Expand Down Expand Up @@ -366,6 +368,9 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
this.specification.buildConfig.filePatterns,
this.specification.buildConfig.ignoredFilePatterns,
)
case 'hosted_app_home':
await this.copyStaticAssets()
break
case 'none':
break
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import uiExtensionSpec from './specifications/ui_extension.js'
import webPixelSpec from './specifications/web_pixel_extension.js'
import editorExtensionCollectionSpecification from './specifications/editor_extension_collection.js'
import channelSpecificationSpec from './specifications/channel.js'
import hostedAppHomeSpec, {HostedAppHomeSpecIdentifier} from './specifications/app_config_hosted_app_home.js'

const SORTED_CONFIGURATION_SPEC_IDENTIFIERS = [
BrandingSpecIdentifier,
Expand All @@ -36,6 +37,7 @@ const SORTED_CONFIGURATION_SPEC_IDENTIFIERS = [
AppProxySpecIdentifier,
PosSpecIdentifier,
AppHomeSpecIdentifier,
HostedAppHomeSpecIdentifier,
]

/**
Expand All @@ -58,6 +60,7 @@ function loadSpecifications() {
appPrivacyComplienceSpec,
appWebhooksSpec,
appWebhookSubscriptionSpec,
hostedAppHomeSpec,
]
const moduleSpecs = [
checkoutPostPurchaseSpec,
Expand Down
6 changes: 5 additions & 1 deletion packages/app/src/cli/models/extensions/specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export interface Asset {
}

type BuildConfig =
| {mode: 'ui' | 'theme' | 'function' | 'tax_calculation' | 'none'}
| {mode: 'ui' | 'theme' | 'function' | 'tax_calculation' | 'none' | 'hosted_app_home'}
| {mode: 'copy_files'; filePatterns: string[]; ignoredFilePatterns?: string[]}
/**
* Extension specification with all the needed properties and methods to load an extension.
Expand Down Expand Up @@ -239,11 +239,13 @@ export function createExtensionSpecification<TConfiguration extends BaseConfigTy
export function createConfigExtensionSpecification<TConfiguration extends BaseConfigType = BaseConfigType>(spec: {
identifier: string
schema: ZodSchemaType<TConfiguration>
buildConfig?: BuildConfig
appModuleFeatures?: (config?: TConfiguration) => ExtensionFeature[]
transformConfig: TransformationConfig | CustomTransformationConfig
uidStrategy?: UidStrategy
getDevSessionUpdateMessages?: (config: TConfiguration) => Promise<string[]>
patchWithAppDevURLs?: (config: TConfiguration, urls: ApplicationURLs) => void
copyStaticAssets?: (config: TConfiguration, directory: string, outputPath: string) => Promise<void>
}): ExtensionSpecification<TConfiguration> {
const appModuleFeatures = spec.appModuleFeatures ?? (() => [])
return createExtensionSpecification({
Expand All @@ -256,8 +258,10 @@ export function createConfigExtensionSpecification<TConfiguration extends BaseCo
transformRemoteToLocal: resolveReverseAppConfigTransform(spec.schema, spec.transformConfig),
experience: 'configuration',
uidStrategy: spec.uidStrategy ?? 'single',
buildConfig: spec.buildConfig ?? {mode: 'none'},
getDevSessionUpdateMessages: spec.getDevSessionUpdateMessages,
patchWithAppDevURLs: spec.patchWithAppDevURLs,
copyStaticAssets: spec.copyStaticAssets,
})
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import spec from './app_config_hosted_app_home.js'
import {placeholderAppConfiguration} from '../../app/app.test-data.js'
import {copyDirectoryContents} from '@shopify/cli-kit/node/fs'
import {describe, expect, test, vi} from 'vitest'

vi.mock('@shopify/cli-kit/node/fs')

describe('hosted_app_home', () => {
describe('transform', () => {
test('should return the transformed object with static_root', () => {
const object = {
static_root: 'public',
}
const appConfigSpec = spec

const result = appConfigSpec.transformLocalToRemote!(object, placeholderAppConfiguration)

expect(result).toMatchObject({
static_root: 'public',
})
})

test('should return empty object when static_root is not provided', () => {
const object = {}
const appConfigSpec = spec

const result = appConfigSpec.transformLocalToRemote!(object, placeholderAppConfiguration)

expect(result).toMatchObject({})
})
})

describe('reverseTransform', () => {
test('should return the reversed transformed object with static_root', () => {
const object = {
static_root: 'public',
}
const appConfigSpec = spec

const result = appConfigSpec.transformRemoteToLocal!(object)

expect(result).toMatchObject({
static_root: 'public',
})
})

test('should return empty object when static_root is not provided', () => {
const object = {}
const appConfigSpec = spec

const result = appConfigSpec.transformRemoteToLocal!(object)

expect(result).toMatchObject({})
})
})

describe('copyStaticAssets', () => {
test('should copy static assets from source to output directory', async () => {
vi.mocked(copyDirectoryContents).mockResolvedValue(undefined)
const config = {static_root: 'public'}
const directory = '/app/root'
const outputPath = '/output/dist/bundle.js'

await spec.copyStaticAssets!(config, directory, outputPath)

expect(copyDirectoryContents).toHaveBeenCalledWith('/app/root/public', '/output/dist')
})

test('should not copy assets when static_root is not provided', async () => {
const config = {}
const directory = '/app/root'
const outputPath = '/output/dist/bundle.js'

await spec.copyStaticAssets!(config, directory, outputPath)

expect(copyDirectoryContents).not.toHaveBeenCalled()
})

test('should throw error when copy fails', async () => {
vi.mocked(copyDirectoryContents).mockRejectedValue(new Error('Permission denied'))
const config = {static_root: 'public'}
const directory = '/app/root'
const outputPath = '/output/dist/bundle.js'

await expect(spec.copyStaticAssets!(config, directory, outputPath)).rejects.toThrow(
'Failed to copy static assets from /app/root/public to /output/dist: Permission denied',
)
})
})

describe('buildConfig', () => {
test('should have hosted_app_home build mode', () => {
expect(spec.buildConfig).toEqual({mode: 'hosted_app_home'})
})
})

describe('identifier', () => {
test('should have correct identifier', () => {
expect(spec.identifier).toBe('hosted_app_home')
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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 HostedAppHomeTransformConfig: TransformationConfig = {
static_root: 'static_root',
}

export const HostedAppHomeSpecIdentifier = 'hosted_app_home'

const hostedAppHomeSpec = createConfigExtensionSpecification({
identifier: HostedAppHomeSpecIdentifier,
buildConfig: {mode: 'hosted_app_home'} as const,
schema: HostedAppHomeSchema,
transformConfig: HostedAppHomeTransformConfig,
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export async function fetchSpecifications({
const extensionSpecifications: FlattenedRemoteSpecification[] = result
.filter((specification) => ['extension', 'configuration'].includes(specification.experience))
.map((spec) => {
console.log('specification', spec.identifier)
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.
Expand Down Expand Up @@ -78,7 +79,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) {
Expand Down
Loading