Skip to content

Commit a3c88ca

Browse files
Remove hosted_app_home from BuildConfig (belongs in add_admin_config_local_spec)
1 parent 4446834 commit a3c88ca

8 files changed

Lines changed: 225 additions & 8 deletions

File tree

packages/app/src/cli/models/extensions/extension-instance.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,14 +364,14 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
364364
this.specification.buildConfig.filePatterns,
365365
this.specification.buildConfig.ignoredFilePatterns,
366366
)
367+
case 'hosted_app_home':
367368
case 'none':
368369
break
369370
}
370371
}
371372

372373
async buildForBundle(options: ExtensionBuildOptions, bundleDirectory: string, outputId?: string) {
373374
this.outputPath = this.getOutputPathForDirectory(bundleDirectory, outputId)
374-
375375
await this.build(options)
376376

377377
const bundleInputPath = joinPath(bundleDirectory, this.getOutputFolderId(outputId))

packages/app/src/cli/models/extensions/specification.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {ZodSchemaType, BaseConfigType, BaseSchema} from './schemas.js'
22
import {ExtensionInstance} from './extension-instance.js'
33
import {blocks} from '../../constants.js'
4+
import {ClientSteps} from '../../services/build/client-steps.js'
45

56
import {Flag} from '../../utilities/developer-platform-client.js'
67
import {AppConfiguration} from '../app/app.js'
@@ -58,8 +59,9 @@ export interface BuildAsset {
5859
}
5960

6061
type BuildConfig =
61-
| {mode: 'ui' | 'theme' | 'function' | 'tax_calculation' | 'none'}
62+
| {mode: 'ui' | 'theme' | 'function' | 'tax_calculation' | 'none' | 'hosted_app_home'}
6263
| {mode: 'copy_files'; filePatterns: string[]; ignoredFilePatterns?: string[]}
64+
6365
/**
6466
* Extension specification with all the needed properties and methods to load an extension.
6567
*/
@@ -73,6 +75,7 @@ export interface ExtensionSpecification<TConfiguration extends BaseConfigType =
7375
surface: string
7476
registrationLimit: number
7577
experience: ExtensionExperience
78+
clientSteps?: ClientSteps
7679
buildConfig: BuildConfig
7780
dependency?: string
7881
graphQLType?: string
@@ -208,6 +211,7 @@ export function createExtensionSpecification<TConfiguration extends BaseConfigTy
208211
experience: spec.experience ?? 'extension',
209212
uidStrategy: spec.uidStrategy ?? (spec.experience === 'configuration' ? 'single' : 'uuid'),
210213
getDevSessionUpdateMessages: spec.getDevSessionUpdateMessages,
214+
clientSteps: spec.clientSteps,
211215
buildConfig: spec.buildConfig ?? {mode: 'none'},
212216
}
213217
const merged = {...defaults, ...spec}
@@ -256,6 +260,8 @@ export function createExtensionSpecification<TConfiguration extends BaseConfigTy
256260
export function createConfigExtensionSpecification<TConfiguration extends BaseConfigType = BaseConfigType>(spec: {
257261
identifier: string
258262
schema: ZodSchemaType<TConfiguration>
263+
clientSteps?: ClientSteps
264+
buildConfig?: BuildConfig
259265
appModuleFeatures?: (config?: TConfiguration) => ExtensionFeature[]
260266
transformConfig: TransformationConfig | CustomTransformationConfig
261267
uidStrategy?: UidStrategy
@@ -273,6 +279,8 @@ export function createConfigExtensionSpecification<TConfiguration extends BaseCo
273279
transformRemoteToLocal: resolveReverseAppConfigTransform(spec.schema, spec.transformConfig),
274280
experience: 'configuration',
275281
uidStrategy: spec.uidStrategy ?? 'single',
282+
clientSteps: spec.clientSteps,
283+
buildConfig: spec.buildConfig ?? {mode: 'none'},
276284
getDevSessionUpdateMessages: spec.getDevSessionUpdateMessages,
277285
patchWithAppDevURLs: spec.patchWithAppDevURLs,
278286
})
@@ -281,15 +289,16 @@ export function createConfigExtensionSpecification<TConfiguration extends BaseCo
281289
export function createContractBasedModuleSpecification<TConfiguration extends BaseConfigType = BaseConfigType>(
282290
spec: Pick<
283291
CreateExtensionSpecType<TConfiguration>,
284-
'identifier' | 'appModuleFeatures' | 'buildConfig' | 'uidStrategy'
292+
'identifier' | 'appModuleFeatures' | 'buildConfig' | 'uidStrategy' | 'clientSteps'
285293
>,
286294
) {
287295
return createExtensionSpecification({
288296
identifier: spec.identifier,
289297
schema: zod.any({}) as unknown as ZodSchemaType<TConfiguration>,
290298
appModuleFeatures: spec.appModuleFeatures,
299+
clientSteps: spec.clientSteps,
291300
buildConfig: spec.buildConfig ?? {mode: 'none'},
292-
uidStrategy: spec.uidStrategy,
301+
uidStrategy: spec.uidStrategy ?? 'single',
293302
deployConfig: async (config, directory) => {
294303
let parsedConfig = configWithoutFirstClassFields(config)
295304
if (spec.appModuleFeatures().includes('localization')) {

packages/app/src/cli/models/extensions/specifications/app_config_privacy_compliance_webhooks.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,13 @@ function relativeUri(uri?: string, appUrl?: string) {
7777
}
7878

7979
function getCustomersDeletionUri(webhooks: WebhooksConfig) {
80-
return getComplianceUri(webhooks, 'customers/redact') || webhooks?.privacy_compliance?.customer_deletion_url
80+
return getComplianceUri(webhooks, 'customers/redact') ?? webhooks?.privacy_compliance?.customer_deletion_url
8181
}
8282

8383
function getCustomersDataRequestUri(webhooks: WebhooksConfig) {
84-
return getComplianceUri(webhooks, 'customers/data_request') || webhooks?.privacy_compliance?.customer_data_request_url
84+
return getComplianceUri(webhooks, 'customers/data_request') ?? webhooks?.privacy_compliance?.customer_data_request_url
8585
}
8686

8787
function getShopDeletionUri(webhooks: WebhooksConfig) {
88-
return getComplianceUri(webhooks, 'shop/redact') || webhooks?.privacy_compliance?.shop_deletion_url
88+
return getComplianceUri(webhooks, 'shop/redact') ?? webhooks?.privacy_compliance?.shop_deletion_url
8989
}

packages/app/src/cli/models/extensions/specifications/payments_app_extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const paymentExtensionSpec = createExtensionSpecification({
6969
)
7070
case CARD_PRESENT_TARGET:
7171
return cardPresentPaymentsAppExtensionDeployConfig(config as CardPresentPaymentsAppExtensionConfigType)
72+
case undefined:
7273
default:
7374
return {}
7475
}

packages/app/src/cli/models/extensions/specifications/tax_calculation.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {createExtensionSpecification} from '../specification.js'
22
import {BaseSchema, MetafieldSchema} from '../schemas.js'
33
import {ExtensionInstance} from '../extension-instance.js'
44
import {zod} from '@shopify/cli-kit/node/schema'
5+
import {joinPath} from '@shopify/cli-kit/node/path'
56

67
const CartLinePropertySchema = zod.object({
78
key: zod.string(),
@@ -31,7 +32,7 @@ const spec = createExtensionSpecification({
3132
schema: TaxCalculationsSchema,
3233
appModuleFeatures: (_) => [],
3334
buildConfig: {mode: 'tax_calculation'},
34-
getOutputRelativePath: (extension: ExtensionInstance<TaxCalculationsConfigType>) => `dist/${extension.handle}.js`,
35+
getOutputRelativePath: (extension: ExtensionInstance<TaxCalculationsConfigType>) => joinPath('dist', `${extension.handle}.js`),
3536
deployConfig: async (config, _) => {
3637
return {
3738
production_api_base_url: config.production_api_base_url,
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {executeStep, BuildContext, LifecycleStep} from './client-steps.js'
2+
import * as stepsIndex from './steps/index.js'
3+
import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
4+
import {beforeEach, describe, expect, test, vi} from 'vitest'
5+
6+
vi.mock('./steps/index.js')
7+
8+
describe('executeStep', () => {
9+
let mockContext: BuildContext
10+
11+
beforeEach(() => {
12+
mockContext = {
13+
extension: {
14+
directory: '/test/dir',
15+
outputPath: '/test/output/index.js',
16+
} as ExtensionInstance,
17+
options: {
18+
stdout: {write: vi.fn()} as any,
19+
stderr: {write: vi.fn()} as any,
20+
app: {} as any,
21+
environment: 'production' as const,
22+
},
23+
stepResults: new Map(),
24+
}
25+
})
26+
27+
const step: LifecycleStep = {
28+
id: 'test-step',
29+
name: 'Test Step',
30+
type: 'include_assets',
31+
config: {},
32+
}
33+
34+
describe('success', () => {
35+
test('returns a successful StepResult with output', async () => {
36+
vi.mocked(stepsIndex.executeStepByType).mockResolvedValue({filesCopied: 3})
37+
38+
const result = await executeStep(step, mockContext)
39+
40+
expect(result.id).toBe('test-step')
41+
expect(result.success).toBe(true)
42+
if (result.success) expect(result.output).toEqual({filesCopied: 3})
43+
expect(result.duration).toBeGreaterThanOrEqual(0)
44+
})
45+
46+
test('logs step execution to stdout', async () => {
47+
vi.mocked(stepsIndex.executeStepByType).mockResolvedValue({})
48+
49+
await executeStep(step, mockContext)
50+
51+
expect(mockContext.options.stdout.write).toHaveBeenCalledWith('Executing step: Test Step\n')
52+
})
53+
})
54+
55+
describe('failure', () => {
56+
test('throws a wrapped error when the step fails', async () => {
57+
vi.mocked(stepsIndex.executeStepByType).mockRejectedValue(new Error('something went wrong'))
58+
59+
await expect(executeStep(step, mockContext)).rejects.toThrow(
60+
'Build step "Test Step" failed: something went wrong',
61+
)
62+
})
63+
64+
test('returns a failure result and logs a warning when continueOnError is true', async () => {
65+
vi.mocked(stepsIndex.executeStepByType).mockRejectedValue(new Error('something went wrong'))
66+
67+
const result = await executeStep({...step, continueOnError: true}, mockContext)
68+
69+
expect(result.success).toBe(false)
70+
if (!result.success) expect(result.error?.message).toBe('something went wrong')
71+
expect(mockContext.options.stderr.write).toHaveBeenCalledWith(
72+
'Warning: Step "Test Step" failed but continuing: something went wrong\n',
73+
)
74+
})
75+
})
76+
})
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import {executeStepByType} from './steps/index.js'
2+
import type {ExtensionInstance} from '../../models/extensions/extension-instance.js'
3+
import type {ExtensionBuildOptions} from './extension.js'
4+
5+
/**
6+
* LifecycleStep represents a single step in the client-side build pipeline.
7+
* Pure configuration object — execution logic is separate (router pattern).
8+
*/
9+
export interface LifecycleStep {
10+
/** Unique identifier, used as the key in the stepResults map */
11+
readonly id: string
12+
13+
/** Human-readable name for logging */
14+
readonly name: string
15+
16+
/** Step type (determines which executor handles it) */
17+
readonly type:
18+
| 'include_assets'
19+
| 'build_theme'
20+
| 'bundle_theme'
21+
| 'bundle_ui'
22+
| 'copy_static_assets'
23+
| 'build_function'
24+
| 'create_tax_stub'
25+
26+
/** Step-specific configuration */
27+
readonly config: {[key: string]: unknown}
28+
29+
/** Whether to continue on error (default: false) */
30+
readonly continueOnError?: boolean
31+
}
32+
33+
/**
34+
* A group of steps scoped to a specific lifecycle phase.
35+
* Allows executing only the steps relevant to a given lifecycle (e.g. 'deploy').
36+
*/
37+
interface ClientLifecycleGroup {
38+
readonly lifecycle: 'deploy'
39+
readonly steps: ReadonlyArray<LifecycleStep>
40+
}
41+
42+
/**
43+
* The full client steps configuration for an extension.
44+
* Replaces the old buildConfig contract.
45+
*/
46+
export type ClientSteps = ReadonlyArray<ClientLifecycleGroup>
47+
48+
/**
49+
* Context passed through the step pipeline.
50+
* Each step can read from and write to the context.
51+
*/
52+
export interface BuildContext {
53+
readonly extension: ExtensionInstance
54+
readonly options: ExtensionBuildOptions
55+
readonly stepResults: Map<string, StepResult>
56+
}
57+
58+
type StepResult = {
59+
readonly id: string
60+
readonly duration: number
61+
} & (
62+
| {
63+
readonly success: false
64+
readonly error: Error
65+
}
66+
| {
67+
readonly success: true
68+
readonly output: never
69+
}
70+
)
71+
72+
/**
73+
* Executes a single client step with error handling.
74+
*/
75+
export async function executeStep(step: LifecycleStep, context: BuildContext): Promise<StepResult> {
76+
const startTime = Date.now()
77+
78+
try {
79+
context.options.stdout.write(`Executing step: ${step.name}\n`)
80+
const output = await executeStepByType(step, context)
81+
82+
return {
83+
id: step.id,
84+
success: true,
85+
duration: Date.now() - startTime,
86+
output: output as never,
87+
}
88+
} catch (error) {
89+
const stepError = error as Error
90+
91+
if (step.continueOnError) {
92+
context.options.stderr.write(`Warning: Step "${step.name}" failed but continuing: ${stepError.message}\n`)
93+
return {
94+
id: step.id,
95+
success: false,
96+
duration: Date.now() - startTime,
97+
error: stepError,
98+
}
99+
}
100+
101+
throw new Error(`Build step "${step.name}" failed: ${stepError.message}`)
102+
}
103+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type {LifecycleStep, BuildContext} from '../client-steps.js'
2+
3+
/**
4+
* Routes step execution to the appropriate handler based on step type.
5+
* This implements the Command Pattern router, dispatching to type-specific executors.
6+
*
7+
* @param step - The build step configuration
8+
* @param context - The build context
9+
* @returns The output from the step execution
10+
* @throws Error if the step type is not implemented or unknown
11+
*/
12+
export async function executeStepByType(step: LifecycleStep, _context: BuildContext): Promise<unknown> {
13+
switch (step.type) {
14+
// Future step types (not implemented yet):
15+
case 'include_assets':
16+
case 'build_theme':
17+
case 'bundle_theme':
18+
case 'bundle_ui':
19+
case 'copy_static_assets':
20+
case 'build_function':
21+
case 'create_tax_stub':
22+
throw new Error(`Build step type "${step.type}" is not yet implemented.`)
23+
24+
default:
25+
throw new Error(`Unknown build step type: ${(step as {type: string}).type}`)
26+
}
27+
}

0 commit comments

Comments
 (0)