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 @@ -22,6 +22,7 @@ import {
bundleFunctionExtension,
} from '../../services/build/extension.js'
import {bundleThemeExtension, copyFilesForExtension} from '../../services/extensions/bundle.js'
import {executeBuildSteps} from '../../services/build/build-steps.js'
import {Identifiers} from '../app/identifiers.js'
import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
import {AppConfigurationWithoutPath} from '../app/app.js'
Expand Down Expand Up @@ -370,9 +371,8 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
this.specification.buildConfig.filePatterns,
this.specification.buildConfig.ignoredFilePatterns,
)
case 'hosted_app_home':
await this.copyStaticAssets()
break
case 'build_steps':
return executeBuildSteps(this, this.specification.buildConfig.stepsConfig, options)
case 'none':
break
}
Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/cli/models/extensions/specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {ZodSchemaType, BaseConfigType, BaseSchema} from './schemas.js'
import {ExtensionInstance} from './extension-instance.js'
import {blocks} from '../../constants.js'
import {BuildStepsConfig} from '../../services/build/build-steps.js'

import {Flag} from '../../utilities/developer-platform-client.js'
import {AppConfigurationWithoutPath} from '../app/app.js'
Expand Down Expand Up @@ -55,8 +56,9 @@ export interface BuildAsset {
}

type BuildConfig =
| {mode: 'ui' | 'theme' | 'function' | 'tax_calculation' | 'none' | 'hosted_app_home'}
| {mode: 'ui' | 'theme' | 'function' | 'tax_calculation' | 'none'}
| {mode: 'copy_files'; filePatterns: string[]; ignoredFilePatterns?: string[]}
| {mode: 'build_steps'; stepsConfig: BuildStepsConfig}
/**
* Extension specification with all the needed properties and methods to load an extension.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
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')
import {ExtensionInstance} from '../extension-instance.js'
import {describe, expect, test} from 'vitest'

describe('hosted_app_home', () => {
describe('transform', () => {
Expand Down Expand Up @@ -54,43 +52,62 @@ describe('hosted_app_home', () => {
})
})

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')
describe('buildConfig', () => {
test('should use build_steps mode', () => {
expect(spec.buildConfig.mode).toBe('build_steps')
})

test('should not copy assets when static_root is not provided', async () => {
const config = {}
const directory = '/app/root'
const outputPath = '/output/dist/bundle.js'
test('should have static stepsConfig (not a function)', () => {
if (spec.buildConfig.mode !== 'build_steps') {
throw new Error('Expected build_steps mode')
}
expect(typeof spec.buildConfig.stepsConfig).toBe('object')
expect(spec.buildConfig.stepsConfig).not.toBeNull()
})

await spec.copyStaticAssets!(config, directory, outputPath)
test('should have copy-static-assets step with configPath reference', () => {
if (spec.buildConfig.mode !== 'build_steps') {
throw new Error('Expected build_steps mode')
}

expect(copyDirectoryContents).not.toHaveBeenCalled()
const stepsConfig = spec.buildConfig.stepsConfig

// Verify the build steps config
expect(stepsConfig.steps).toHaveLength(1)
expect(stepsConfig.steps[0]).toMatchObject({
id: 'copy-static-assets',
displayName: 'Copy Static Assets',
type: 'copy_files',
config: {
strategy: 'directory',
source: {configPath: 'static_root', optional: true}, // ← Uses configPath reference with optional flag
},
})
expect(stepsConfig.stopOnError).toBe(true)
})

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'
test('should not have skip callback (must be serializable)', () => {
if (spec.buildConfig.mode !== 'build_steps') {
throw new Error('Expected build_steps mode')
}

await expect(spec.copyStaticAssets!(config, directory, outputPath)).rejects.toThrow(
'Failed to copy static assets from /app/root/public to /output/dist: Permission denied',
)
const step = spec.buildConfig.stepsConfig.steps[0]
// No skip function - config must be pure data (serializable)
expect(step?.skip).toBeUndefined()
})
})

describe('buildConfig', () => {
test('should have hosted_app_home build mode', () => {
expect(spec.buildConfig).toEqual({mode: 'hosted_app_home'})
test('config should be serializable to JSON', () => {
if (spec.buildConfig.mode !== 'build_steps') {
throw new Error('Expected build_steps mode')
}

// Should be able to serialize and deserialize without errors
const serialized = JSON.stringify(spec.buildConfig.stepsConfig)
expect(serialized).toBeDefined()

const deserialized = JSON.parse(serialized)
expect(deserialized.steps).toHaveLength(1)
expect(deserialized.steps[0].config.source).toEqual({configPath: 'static_root', optional: true})
})
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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 {BuildStepsConfig} from '../../../services/build/build-steps.js'
import {zod} from '@shopify/cli-kit/node/schema'

const HostedAppHomeSchema = BaseSchemaWithoutHandle.extend({
Expand All @@ -14,20 +13,39 @@ const HostedAppHomeTransformConfig: TransformationConfig = {

export const HostedAppHomeSpecIdentifier = 'hosted_app_home'

/**
* Static build steps configuration for hosted_app_home.
* Uses ConfigurableValue to reference the 'static_root' field from the TOML config.
* This configuration is pure data (no functions) and can be serialized to/from JSON.
*
* When static_root is not configured in TOML, the configPath will resolve to undefined,
* and the copy-files-step will handle it gracefully (skip with optional: true).
*/
const hostedAppHomeBuildSteps: BuildStepsConfig = {
steps: [
{
id: 'copy-static-assets',
displayName: 'Copy Static Assets',
type: 'copy_files',
config: {
strategy: 'directory',
// Reference to TOML config field - resolves to undefined if not configured
// optional: true means skip silently if the field doesn't exist in TOML
source: {configPath: 'static_root', optional: true},
},
},
],
stopOnError: true,
}

const hostedAppHomeSpec = createConfigExtensionSpecification({
identifier: HostedAppHomeSpecIdentifier,
buildConfig: {mode: 'hosted_app_home'} as const,
buildConfig: {
mode: 'build_steps',
stepsConfig: hostedAppHomeBuildSteps,
},
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
Loading
Loading