diff --git a/packages/app/src/cli/models/extensions/extension-instance.ts b/packages/app/src/cli/models/extensions/extension-instance.ts index c51ee23bbe..932033f315 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.ts @@ -13,6 +13,7 @@ import {PrivacyComplianceWebhooksSpecIdentifier} from './specifications/app_conf import {WebhooksSpecIdentifier} from './specifications/app_config_webhook.js' import {WebhookSubscriptionSpecIdentifier} from './specifications/app_config_webhook_subscription.js' import {EventsSpecIdentifier} from './specifications/app_config_events.js' +import {HostedAppHomeSpecIdentifier} from './specifications/app_config_hosted_app_home.js' import { ExtensionBuildOptions, buildFunctionExtension, @@ -41,6 +42,7 @@ export const CONFIG_EXTENSION_IDS: string[] = [ AppHomeSpecIdentifier, AppProxySpecIdentifier, BrandingSpecIdentifier, + HostedAppHomeSpecIdentifier, PosSpecIdentifier, PrivacyComplianceWebhooksSpecIdentifier, WebhookSubscriptionSpecIdentifier, @@ -368,6 +370,9 @@ 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({ @@ -262,8 +264,10 @@ export function createConfigExtensionSpecification { + 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') + }) + }) +}) 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 0000000000..6b71b71049 --- /dev/null +++ b/packages/app/src/cli/models/extensions/specifications/app_config_hosted_app_home.ts @@ -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