Skip to content
Open
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
13 changes: 7 additions & 6 deletions packages/app/src/cli/models/extensions/specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ export interface BuildAsset {
static?: boolean
}

type BuildConfig =
| {mode: 'none'}
| {mode: 'ui' | 'theme' | 'function' | 'tax_calculation' | 'copy_files'; steps: ReadonlyArray<BuildStep>}
interface BuildConfig {
mode: 'ui' | 'theme' | 'function' | 'tax_calculation' | 'copy_files' | 'none'
steps: ReadonlyArray<BuildStep>
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BuildConfig is now a breaking change: mode: 'none' requires steps

Previously BuildConfig was a union where {mode: 'none'} did not include steps. This PR changes it to an interface with mandatory steps for all modes, including none. Existing specs or runtime payloads that omit steps for none will fail type-checking or force dummy steps: [] everywhere.

Evidence: helper defaults updated to {mode: 'none', steps: []} only fixes callers using helpers; other constructors/deserializers remain broken.

Impact: TS compile failures and downstream breakage; may block builds/releases; affects any extension spec using {mode:'none'} without steps.

/**
* Extension specification with all the needed properties and methods to load an extension.
*/
Expand Down Expand Up @@ -204,7 +205,7 @@ export function createExtensionSpecification<TConfiguration extends BaseConfigTy
experience: spec.experience ?? 'extension',
uidStrategy: spec.uidStrategy ?? (spec.experience === 'configuration' ? 'single' : 'uuid'),
getDevSessionUpdateMessages: spec.getDevSessionUpdateMessages,
buildConfig: spec.buildConfig ?? {mode: 'none'},
buildConfig: spec.buildConfig ?? {mode: 'none', steps: []},
}
const merged = {...defaults, ...spec}

Expand Down Expand Up @@ -265,7 +266,7 @@ export function createConfigExtensionSpecification<TConfiguration extends BaseCo
transformRemoteToLocal: resolveReverseAppConfigTransform(spec.schema, spec.transformConfig),
experience: 'configuration',
uidStrategy: spec.uidStrategy ?? 'single',
buildConfig: spec.buildConfig ?? {mode: 'none'},
buildConfig: spec.buildConfig ?? {mode: 'none', steps: []},
getDevSessionUpdateMessages: spec.getDevSessionUpdateMessages,
patchWithAppDevURLs: spec.patchWithAppDevURLs,
copyStaticAssets: spec.copyStaticAssets,
Expand All @@ -279,7 +280,7 @@ export function createContractBasedModuleSpecification<TConfiguration extends Ba
identifier: spec.identifier,
schema: zod.any({}) as unknown as ZodSchemaType<TConfiguration>,
appModuleFeatures: spec.appModuleFeatures,
buildConfig: spec.buildConfig ?? {mode: 'none'},
buildConfig: spec.buildConfig ?? {mode: 'none', steps: []},
deployConfig: async (config, directory) => {
let parsedConfig = configWithoutFirstClassFields(config)
if (spec.appModuleFeatures().includes('localization')) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
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 {describe, expect, test} from 'vitest'

describe('hosted_app_home', () => {
describe('transform', () => {
Expand Down Expand Up @@ -54,43 +51,42 @@ 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 copy_files mode', () => {
expect(spec.buildConfig.mode).toBe('copy_files')
})

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)
test('should have copy-static-assets step with tomlKey entry', () => {
if (spec.buildConfig.mode === 'none') {
throw new Error('Expected build_steps mode')
}

expect(copyDirectoryContents).not.toHaveBeenCalled()
expect(spec.buildConfig.steps).toHaveLength(1)
expect(spec.buildConfig.steps[0]).toMatchObject({
id: 'copy-static-assets',
displayName: 'Copy Static Assets',
type: 'copy_files',
config: {
strategy: 'files',
definition: {files: [{tomlKey: 'static_root'}]},
},
})
})

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('config should be serializable to JSON', () => {
if (spec.buildConfig.mode === 'none') {
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 serialized = JSON.stringify(spec.buildConfig)
expect(serialized).toBeDefined()

describe('buildConfig', () => {
test('should have hosted_app_home build mode', () => {
expect(spec.buildConfig).toEqual({mode: 'none'})
const deserialized = JSON.parse(serialized)
expect(deserialized.steps).toHaveLength(1)
expect(deserialized.steps[0].config).toEqual({
strategy: 'files',
definition: {files: [{tomlKey: 'static_root'}]},
})
})
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
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({
Expand All @@ -16,18 +14,24 @@ export const HostedAppHomeSpecIdentifier = 'hosted_app_home'

const hostedAppHomeSpec = createConfigExtensionSpecification({
identifier: HostedAppHomeSpecIdentifier,
buildConfig: {mode: 'none'} as const,
buildConfig: {
mode: 'copy_files',
steps: [
{
id: 'copy-static-assets',
displayName: 'Copy Static Assets',
type: 'copy_files',
config: {
strategy: 'files',
definition: {
files: [{tomlKey: 'static_root'}],
},
},
},
],
},
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