diff --git a/packages/app/src/cli/models/extensions/extension-instance.test.ts b/packages/app/src/cli/models/extensions/extension-instance.test.ts index 5a808958df9..dbafca2026a 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.test.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.test.ts @@ -72,10 +72,10 @@ describe('keepBuiltSourcemapsLocally', async () => { await extensionInstance.keepBuiltSourcemapsLocally(bundleDirectory) - expect(fileExistsSync(joinPath(outputPath, 'dist', 'scriptToMove.js'))).toBe(false) - expect(fileExistsSync(joinPath(outputPath, 'dist', 'scriptToMove.js.map'))).toBe(true) - expect(fileExistsSync(joinPath(outputPath, 'dist', 'scriptToIgnore.js'))).toBe(false) - expect(fileExistsSync(joinPath(outputPath, 'dist', 'scriptToIgnore.js.map'))).toBe(false) + expect(fileExistsSync(joinPath(outputPath, 'uid1', 'scriptToMove.js'))).toBe(false) + expect(fileExistsSync(joinPath(outputPath, 'uid1', 'scriptToMove.js.map'))).toBe(true) + expect(fileExistsSync(joinPath(outputPath, 'otherUID', 'scriptToIgnore.js'))).toBe(false) + expect(fileExistsSync(joinPath(outputPath, 'otherUID', 'scriptToIgnore.js.map'))).toBe(false) }) }) }) @@ -103,10 +103,10 @@ describe('keepBuiltSourcemapsLocally', async () => { await extensionInstance.keepBuiltSourcemapsLocally(bundleInputPath) - expect(fileExistsSync(joinPath(outputPath, 'dist', 'scriptToMove.js'))).toBe(false) - expect(fileExistsSync(joinPath(outputPath, 'dist', 'scriptToMove.js.map'))).toBe(false) - expect(fileExistsSync(joinPath(outputPath, 'dist', 'scriptToIgnore.js'))).toBe(false) - expect(fileExistsSync(joinPath(outputPath, 'dist', 'scriptToIgnore.js.map'))).toBe(false) + expect(fileExistsSync(joinPath(outputPath, 'wrongUID', 'scriptToMove.js'))).toBe(false) + expect(fileExistsSync(joinPath(outputPath, 'wrongUID', 'scriptToMove.js.map'))).toBe(false) + expect(fileExistsSync(joinPath(outputPath, 'otherUID', 'scriptToIgnore.js'))).toBe(false) + expect(fileExistsSync(joinPath(outputPath, 'otherUID', 'scriptToIgnore.js.map'))).toBe(false) }) }) }) @@ -133,10 +133,10 @@ describe('keepBuiltSourcemapsLocally', async () => { await extensionInstance.keepBuiltSourcemapsLocally(bundleDirectory) - expect(fileExistsSync(joinPath(outputPath, 'dist', 'scriptToMove.js'))).toBe(false) - expect(fileExistsSync(joinPath(outputPath, 'dist', 'scriptToMove.js.map'))).toBe(false) - expect(fileExistsSync(joinPath(outputPath, 'dist', 'scriptToIgnore.js'))).toBe(false) - expect(fileExistsSync(joinPath(outputPath, 'dist', 'scriptToIgnore.js.map'))).toBe(false) + expect(fileExistsSync(joinPath(outputPath, 'uid1', 'scriptToMove.js'))).toBe(false) + expect(fileExistsSync(joinPath(outputPath, 'uid1', 'scriptToMove.js.map'))).toBe(false) + expect(fileExistsSync(joinPath(outputPath, 'otherUID', 'scriptToIgnore.js'))).toBe(false) + expect(fileExistsSync(joinPath(outputPath, 'otherUID', 'scriptToIgnore.js.map'))).toBe(false) }) }) }) diff --git a/packages/app/src/cli/models/extensions/extension-instance.ts b/packages/app/src/cli/models/extensions/extension-instance.ts index c292a95f8b2..ebc742ad74a 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.ts @@ -27,7 +27,7 @@ import {ok} from '@shopify/cli-kit/node/result' import {constantize, slugify} from '@shopify/cli-kit/common/string' import {hashString, nonRandomUUID} from '@shopify/cli-kit/node/crypto' import {partnersFqdn} from '@shopify/cli-kit/node/context/fqdn' -import {joinPath, basename, normalizePath, resolvePath} from '@shopify/cli-kit/node/path' +import {joinPath, normalizePath, resolvePath, relativePath, basename} from '@shopify/cli-kit/node/path' import {fileExists, touchFile, moveFile, writeFile, glob, copyFile, globSync} from '@shopify/cli-kit/node/fs' import {getPathValue} from '@shopify/cli-kit/common/object' import {outputDebug} from '@shopify/cli-kit/node/output' @@ -143,14 +143,11 @@ export class ExtensionInstance) => string getBundleExtensionStdinContent?: (config: TConfiguration) => {main: string; assets?: Asset[]} deployConfig?: ( config: TConfiguration, @@ -207,6 +211,7 @@ export function createExtensionSpecification(spec: { identifier: string schema: ZodSchemaType + clientSteps?: ClientSteps + buildConfig?: BuildConfig appModuleFeatures?: (config?: TConfiguration) => ExtensionFeature[] transformConfig: TransformationConfig | CustomTransformationConfig uidStrategy?: UidStrategy @@ -272,6 +279,8 @@ export function createConfigExtensionSpecification, appModuleFeatures: spec.appModuleFeatures, + clientSteps: spec.clientSteps, buildConfig: spec.buildConfig ?? {mode: 'none'}, uidStrategy: spec.uidStrategy, deployConfig: async (config, directory) => { diff --git a/packages/app/src/cli/models/extensions/specifications/app_config_privacy_compliance_webhooks.ts b/packages/app/src/cli/models/extensions/specifications/app_config_privacy_compliance_webhooks.ts index fbeada25f8c..3139ad3c0e6 100644 --- a/packages/app/src/cli/models/extensions/specifications/app_config_privacy_compliance_webhooks.ts +++ b/packages/app/src/cli/models/extensions/specifications/app_config_privacy_compliance_webhooks.ts @@ -77,13 +77,13 @@ function relativeUri(uri?: string, appUrl?: string) { } function getCustomersDeletionUri(webhooks: WebhooksConfig) { - return getComplianceUri(webhooks, 'customers/redact') || webhooks?.privacy_compliance?.customer_deletion_url + return getComplianceUri(webhooks, 'customers/redact') ?? webhooks?.privacy_compliance?.customer_deletion_url } function getCustomersDataRequestUri(webhooks: WebhooksConfig) { - return getComplianceUri(webhooks, 'customers/data_request') || webhooks?.privacy_compliance?.customer_data_request_url + return getComplianceUri(webhooks, 'customers/data_request') ?? webhooks?.privacy_compliance?.customer_data_request_url } function getShopDeletionUri(webhooks: WebhooksConfig) { - return getComplianceUri(webhooks, 'shop/redact') || webhooks?.privacy_compliance?.shop_deletion_url + return getComplianceUri(webhooks, 'shop/redact') ?? webhooks?.privacy_compliance?.shop_deletion_url } diff --git a/packages/app/src/cli/models/extensions/specifications/checkout_post_purchase.ts b/packages/app/src/cli/models/extensions/specifications/checkout_post_purchase.ts index 616adf80b73..cbae1290736 100644 --- a/packages/app/src/cli/models/extensions/specifications/checkout_post_purchase.ts +++ b/packages/app/src/cli/models/extensions/specifications/checkout_post_purchase.ts @@ -1,9 +1,11 @@ +import {ExtensionInstance} from '../extension-instance.js' import {BaseSchema, MetafieldSchema} from '../schemas.js' import {createExtensionSpecification} from '../specification.js' import {zod} from '@shopify/cli-kit/node/schema' const dependency = '@shopify/post-purchase-ui-extensions' +type CheckoutPostPurchaseConfigType = zod.infer const CheckoutPostPurchaseSchema = BaseSchema.extend({ metafields: zod.array(MetafieldSchema).optional(), }) @@ -15,6 +17,8 @@ const checkoutPostPurchaseSpec = createExtensionSpecification({ schema: CheckoutPostPurchaseSchema, appModuleFeatures: (_) => ['ui_preview', 'cart_url', 'esbuild', 'single_js_entry_path'], buildConfig: {mode: 'ui'}, + getOutputRelativePath: (extension: ExtensionInstance) => + `dist/${extension.handle}.js`, deployConfig: async (config, _) => { return {metafields: config.metafields ?? []} }, diff --git a/packages/app/src/cli/models/extensions/specifications/checkout_ui_extension.ts b/packages/app/src/cli/models/extensions/specifications/checkout_ui_extension.ts index f08dfd97c40..ab37ad6a44f 100644 --- a/packages/app/src/cli/models/extensions/specifications/checkout_ui_extension.ts +++ b/packages/app/src/cli/models/extensions/specifications/checkout_ui_extension.ts @@ -1,10 +1,12 @@ import {createExtensionSpecification} from '../specification.js' import {BaseSchema, MetafieldSchema} from '../schemas.js' import {loadLocalesConfig} from '../../../utilities/extensions/locales-configuration.js' +import {ExtensionInstance} from '../extension-instance.js' import {zod} from '@shopify/cli-kit/node/schema' const dependency = '@shopify/checkout-ui-extensions' +type CheckoutConfigType = zod.infer const CheckoutSchema = BaseSchema.extend({ name: zod.string(), extension_points: zod.array(zod.string()).optional(), @@ -22,6 +24,7 @@ const checkoutSpec = createExtensionSpecification({ schema: CheckoutSchema, appModuleFeatures: (_) => ['ui_preview', 'cart_url', 'esbuild', 'single_js_entry_path', 'generates_source_maps'], buildConfig: {mode: 'ui'}, + getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, deployConfig: async (config, directory) => { return { extension_points: config.extension_points, diff --git a/packages/app/src/cli/models/extensions/specifications/function.ts b/packages/app/src/cli/models/extensions/specifications/function.ts index ce6d2ec5a5d..c1f80ee796e 100644 --- a/packages/app/src/cli/models/extensions/specifications/function.ts +++ b/packages/app/src/cli/models/extensions/specifications/function.ts @@ -1,6 +1,7 @@ import {createExtensionSpecification} from '../specification.js' import {BaseSchema} from '../schemas.js' import {loadLocalesConfig} from '../../../utilities/extensions/locales-configuration.js' +import {ExtensionInstance} from '../extension-instance.js' import {zod} from '@shopify/cli-kit/node/schema' import {joinPath} from '@shopify/cli-kit/node/path' import {fileExists, readFile} from '@shopify/cli-kit/node/fs' @@ -88,6 +89,8 @@ const functionSpec = createExtensionSpecification({ schema: FunctionExtensionSchema, appModuleFeatures: (_) => ['function'], buildConfig: {mode: 'function'}, + getOutputRelativePath: (extension: ExtensionInstance) => + extension.configuration.build?.path ?? joinPath('dist', 'index.wasm'), deployConfig: async (config, directory, apiKey) => { let inputQuery: string | undefined const moduleId = randomUUID() diff --git a/packages/app/src/cli/models/extensions/specifications/payments_app_extension.ts b/packages/app/src/cli/models/extensions/specifications/payments_app_extension.ts index 33dfeda8b2e..e27aaab8655 100644 --- a/packages/app/src/cli/models/extensions/specifications/payments_app_extension.ts +++ b/packages/app/src/cli/models/extensions/specifications/payments_app_extension.ts @@ -69,6 +69,7 @@ const paymentExtensionSpec = createExtensionSpecification({ ) case CARD_PRESENT_TARGET: return cardPresentPaymentsAppExtensionDeployConfig(config as CardPresentPaymentsAppExtensionConfigType) + case undefined: default: return {} } diff --git a/packages/app/src/cli/models/extensions/specifications/pos_ui_extension.ts b/packages/app/src/cli/models/extensions/specifications/pos_ui_extension.ts index 33962306a24..2d9dc37b4ee 100644 --- a/packages/app/src/cli/models/extensions/specifications/pos_ui_extension.ts +++ b/packages/app/src/cli/models/extensions/specifications/pos_ui_extension.ts @@ -1,17 +1,22 @@ import {getDependencyVersion} from '../../app/app.js' import {createExtensionSpecification} from '../specification.js' import {BaseSchema} from '../schemas.js' +import {ExtensionInstance} from '../extension-instance.js' import {BugError} from '@shopify/cli-kit/node/error' import {zod} from '@shopify/cli-kit/node/schema' const dependency = '@shopify/retail-ui-extensions' +type PosUIConfigType = zod.infer +const PosUISchema = BaseSchema.extend({name: zod.string()}) + const posUISpec = createExtensionSpecification({ identifier: 'pos_ui_extension', dependency, - schema: BaseSchema.extend({name: zod.string()}), + schema: PosUISchema, appModuleFeatures: (_) => ['ui_preview', 'esbuild', 'single_js_entry_path'], buildConfig: {mode: 'ui'}, + getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, deployConfig: async (config, directory) => { const result = await getDependencyVersion(dependency, directory) if (result === 'not_found') throw new BugError(`Dependency ${dependency} not found`) diff --git a/packages/app/src/cli/models/extensions/specifications/product_subscription.ts b/packages/app/src/cli/models/extensions/specifications/product_subscription.ts index ba807e409f6..2497a236130 100644 --- a/packages/app/src/cli/models/extensions/specifications/product_subscription.ts +++ b/packages/app/src/cli/models/extensions/specifications/product_subscription.ts @@ -1,10 +1,14 @@ import {getDependencyVersion} from '../../app/app.js' import {createExtensionSpecification} from '../specification.js' import {BaseSchema} from '../schemas.js' +import {ExtensionInstance} from '../extension-instance.js' import {BugError} from '@shopify/cli-kit/node/error' +import {zod} from '@shopify/cli-kit/node/schema' const dependency = '@shopify/admin-ui-extensions' +type ProductSubscriptionConfigType = zod.infer + const productSubscriptionSpec = createExtensionSpecification({ identifier: 'product_subscription', additionalIdentifiers: ['subscription_management'], @@ -13,6 +17,7 @@ const productSubscriptionSpec = createExtensionSpecification({ schema: BaseSchema, appModuleFeatures: (_) => ['ui_preview', 'esbuild', 'single_js_entry_path'], buildConfig: {mode: 'ui'}, + getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, deployConfig: async (_, directory) => { const result = await getDependencyVersion(dependency, directory) if (result === 'not_found') throw new BugError(`Dependency ${dependency} not found`) diff --git a/packages/app/src/cli/models/extensions/specifications/tax_calculation.ts b/packages/app/src/cli/models/extensions/specifications/tax_calculation.ts index 1e97e577bb6..e7d09509939 100644 --- a/packages/app/src/cli/models/extensions/specifications/tax_calculation.ts +++ b/packages/app/src/cli/models/extensions/specifications/tax_calculation.ts @@ -1,11 +1,13 @@ import {createExtensionSpecification} from '../specification.js' import {BaseSchema, MetafieldSchema} from '../schemas.js' +import {ExtensionInstance} from '../extension-instance.js' import {zod} from '@shopify/cli-kit/node/schema' const CartLinePropertySchema = zod.object({ key: zod.string(), }) +type TaxCalculationsConfigType = zod.infer const TaxCalculationsSchema = BaseSchema.extend({ production_api_base_url: zod.string(), benchmark_api_base_url: zod.string().optional(), @@ -29,6 +31,7 @@ const spec = createExtensionSpecification({ schema: TaxCalculationsSchema, appModuleFeatures: (_) => [], buildConfig: {mode: 'tax_calculation'}, + getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, deployConfig: async (config, _) => { return { production_api_base_url: config.production_api_base_url, diff --git a/packages/app/src/cli/models/extensions/specifications/ui_extension.ts b/packages/app/src/cli/models/extensions/specifications/ui_extension.ts index f3e04cbeab9..8f8dc83d02a 100644 --- a/packages/app/src/cli/models/extensions/specifications/ui_extension.ts +++ b/packages/app/src/cli/models/extensions/specifications/ui_extension.ts @@ -36,6 +36,7 @@ export interface BuildManifest { const missingExtensionPointsMessage = 'No extension targets defined, add a `targeting` field to your configuration' +type UIExtensionConfigType = zod.infer export const UIExtensionSchema = BaseSchema.extend({ name: zod.string(), type: zod.literal('ui_extension'), @@ -102,6 +103,7 @@ const uiExtensionSpec = createExtensionSpecification({ dependency, schema: UIExtensionSchema, buildConfig: {mode: 'ui'}, + getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, appModuleFeatures: (config) => { const basic: ExtensionFeature[] = ['ui_preview', 'esbuild', 'generates_source_maps'] const needsCart = diff --git a/packages/app/src/cli/models/extensions/specifications/web_pixel_extension.ts b/packages/app/src/cli/models/extensions/specifications/web_pixel_extension.ts index 298a18d876b..e512e66e5f4 100644 --- a/packages/app/src/cli/models/extensions/specifications/web_pixel_extension.ts +++ b/packages/app/src/cli/models/extensions/specifications/web_pixel_extension.ts @@ -1,5 +1,6 @@ import {createExtensionSpecification} from '../specification.js' import {BaseSchema} from '../schemas.js' +import {ExtensionInstance} from '../extension-instance.js' import {zod} from '@shopify/cli-kit/node/schema' import {AbortError} from '@shopify/cli-kit/node/error' import {fileSize} from '@shopify/cli-kit/node/fs' @@ -10,6 +11,7 @@ const BUNDLE_SIZE_LIMIT = BUNDLE_SIZE_LIMIT_KB * kilobytes const dependency = '@shopify/web-pixels-extension' +type WebPixelConfigType = zod.infer const WebPixelSchema = BaseSchema.extend({ runtime_context: zod.string(), version: zod.string().optional(), @@ -32,6 +34,7 @@ const webPixelSpec = createExtensionSpecification({ schema: WebPixelSchema, appModuleFeatures: (_) => ['esbuild', 'single_js_entry_path'], buildConfig: {mode: 'ui'}, + getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, deployConfig: async (config, _) => { return { runtime_context: config.runtime_context, diff --git a/packages/app/src/cli/services/build/client-steps.test.ts b/packages/app/src/cli/services/build/client-steps.test.ts new file mode 100644 index 00000000000..3c33e94891b --- /dev/null +++ b/packages/app/src/cli/services/build/client-steps.test.ts @@ -0,0 +1,76 @@ +import {executeStep, BuildContext, LifecycleStep} from './client-steps.js' +import * as stepsIndex from './steps/index.js' +import {ExtensionInstance} from '../../models/extensions/extension-instance.js' +import {beforeEach, describe, expect, test, vi} from 'vitest' + +vi.mock('./steps/index.js') + +describe('executeStep', () => { + let mockContext: BuildContext + + beforeEach(() => { + mockContext = { + extension: { + directory: '/test/dir', + outputPath: '/test/output/index.js', + } as ExtensionInstance, + options: { + stdout: {write: vi.fn()} as any, + stderr: {write: vi.fn()} as any, + app: {} as any, + environment: 'production' as const, + }, + stepResults: new Map(), + } + }) + + const step: LifecycleStep = { + id: 'test-step', + name: 'Test Step', + type: 'include_assets', + config: {}, + } + + describe('success', () => { + test('returns a successful StepResult with output', async () => { + vi.mocked(stepsIndex.executeStepByType).mockResolvedValue({filesCopied: 3}) + + const result = await executeStep(step, mockContext) + + expect(result.id).toBe('test-step') + expect(result.success).toBe(true) + if (result.success) expect(result.output).toEqual({filesCopied: 3}) + expect(result.duration).toBeGreaterThanOrEqual(0) + }) + + test('logs step execution to stdout', async () => { + vi.mocked(stepsIndex.executeStepByType).mockResolvedValue({}) + + await executeStep(step, mockContext) + + expect(mockContext.options.stdout.write).toHaveBeenCalledWith('Executing step: Test Step\n') + }) + }) + + describe('failure', () => { + test('throws a wrapped error when the step fails', async () => { + vi.mocked(stepsIndex.executeStepByType).mockRejectedValue(new Error('something went wrong')) + + await expect(executeStep(step, mockContext)).rejects.toThrow( + 'Build step "Test Step" failed: something went wrong', + ) + }) + + test('returns a failure result and logs a warning when continueOnError is true', async () => { + vi.mocked(stepsIndex.executeStepByType).mockRejectedValue(new Error('something went wrong')) + + const result = await executeStep({...step, continueOnError: true}, mockContext) + + expect(result.success).toBe(false) + if (!result.success) expect(result.error?.message).toBe('something went wrong') + expect(mockContext.options.stderr.write).toHaveBeenCalledWith( + 'Warning: Step "Test Step" failed but continuing: something went wrong\n', + ) + }) + }) +}) diff --git a/packages/app/src/cli/services/build/client-steps.ts b/packages/app/src/cli/services/build/client-steps.ts new file mode 100644 index 00000000000..b84b6bbc35a --- /dev/null +++ b/packages/app/src/cli/services/build/client-steps.ts @@ -0,0 +1,103 @@ +import {executeStepByType} from './steps/index.js' +import type {ExtensionInstance} from '../../models/extensions/extension-instance.js' +import type {ExtensionBuildOptions} from './extension.js' + +/** + * LifecycleStep represents a single step in the client-side build pipeline. + * Pure configuration object — execution logic is separate (router pattern). + */ +export interface LifecycleStep { + /** Unique identifier, used as the key in the stepResults map */ + readonly id: string + + /** Human-readable name for logging */ + readonly name: string + + /** Step type (determines which executor handles it) */ + readonly type: + | 'include_assets' + | 'build_theme' + | 'bundle_theme' + | 'bundle_ui' + | 'copy_static_assets' + | 'build_function' + | 'create_tax_stub' + + /** Step-specific configuration */ + readonly config: {[key: string]: unknown} + + /** Whether to continue on error (default: false) */ + readonly continueOnError?: boolean +} + +/** + * A group of steps scoped to a specific lifecycle phase. + * Allows executing only the steps relevant to a given lifecycle (e.g. 'deploy'). + */ +interface ClientLifecycleGroup { + readonly lifecycle: 'deploy' + readonly steps: ReadonlyArray +} + +/** + * The full client steps configuration for an extension. + * Replaces the old buildConfig contract. + */ +export type ClientSteps = ReadonlyArray + +/** + * Context passed through the step pipeline. + * Each step can read from and write to the context. + */ +export interface BuildContext { + readonly extension: ExtensionInstance + readonly options: ExtensionBuildOptions + readonly stepResults: Map +} + +type StepResult = { + readonly id: string + readonly duration: number +} & ( + | { + readonly success: false + readonly error: Error + } + | { + readonly success: true + readonly output: never + } +) + +/** + * Executes a single client step with error handling. + */ +export async function executeStep(step: LifecycleStep, context: BuildContext): Promise { + const startTime = Date.now() + + try { + context.options.stdout.write(`Executing step: ${step.name}\n`) + const output = await executeStepByType(step, context) + + return { + id: step.id, + success: true, + duration: Date.now() - startTime, + output: output as never, + } + } catch (error) { + const stepError = error as Error + + if (step.continueOnError) { + context.options.stderr.write(`Warning: Step "${step.name}" failed but continuing: ${stepError.message}\n`) + return { + id: step.id, + success: false, + duration: Date.now() - startTime, + error: stepError, + } + } + + throw new Error(`Build step "${step.name}" failed: ${stepError.message}`) + } +} diff --git a/packages/app/src/cli/services/build/extension.ts b/packages/app/src/cli/services/build/extension.ts index 21072a23f66..d1000080051 100644 --- a/packages/app/src/cli/services/build/extension.ts +++ b/packages/app/src/cli/services/build/extension.ts @@ -150,8 +150,7 @@ export async function buildFunctionExtension( try { const bundlePath = extension.outputPath - const relativeBuildPath = - (extension as ExtensionInstance).configuration.build?.path ?? joinPath('dist', 'index.wasm') + const relativeBuildPath = extension.specification.getOutputRelativePath?.(extension) ?? '' extension.outputPath = joinPath(extension.directory, relativeBuildPath) diff --git a/packages/app/src/cli/services/build/steps/index.ts b/packages/app/src/cli/services/build/steps/index.ts new file mode 100644 index 00000000000..846a4d4147b --- /dev/null +++ b/packages/app/src/cli/services/build/steps/index.ts @@ -0,0 +1,27 @@ +import type {LifecycleStep, BuildContext} from '../client-steps.js' + +/** + * Routes step execution to the appropriate handler based on step type. + * This implements the Command Pattern router, dispatching to type-specific executors. + * + * @param step - The build step configuration + * @param context - The build context + * @returns The output from the step execution + * @throws Error if the step type is not implemented or unknown + */ +export async function executeStepByType(step: LifecycleStep, _context: BuildContext): Promise { + switch (step.type) { + // Future step types (not implemented yet): + case 'include_assets': + case 'build_theme': + case 'bundle_theme': + case 'bundle_ui': + case 'copy_static_assets': + case 'build_function': + case 'create_tax_stub': + throw new Error(`Build step type "${step.type}" is not yet implemented.`) + + default: + throw new Error(`Unknown build step type: ${(step as {type: string}).type}`) + } +} diff --git a/packages/app/src/cli/services/dev/extension/server/middlewares.ts b/packages/app/src/cli/services/dev/extension/server/middlewares.ts index 7bf8116bc11..9cec5a369c4 100644 --- a/packages/app/src/cli/services/dev/extension/server/middlewares.ts +++ b/packages/app/src/cli/services/dev/extension/server/middlewares.ts @@ -5,7 +5,7 @@ import {getHTML} from '../templates.js' import {getWebSocketUrl} from '../../extension.js' import {fileExists, isDirectory, readFile, findPathUp} from '@shopify/cli-kit/node/fs' import {IncomingMessage, ServerResponse, sendRedirect, send} from 'h3' -import {joinPath, extname, moduleDirectory} from '@shopify/cli-kit/node/path' +import {joinPath, dirname, extname, moduleDirectory} from '@shopify/cli-kit/node/path' import {outputDebug} from '@shopify/cli-kit/node/output' export function corsMiddleware(_request: IncomingMessage, response: ServerResponse, next: (err?: Error) => unknown) { @@ -90,7 +90,7 @@ export function getExtensionAssetMiddleware({devOptions, getExtensions}: GetExte const bundlePath = devOptions.appWatcher.buildOutputPath const extensionOutputPath = extension.getOutputPathForDirectory(bundlePath) - const buildDirectory = extensionOutputPath.replace(extension.outputFileName, '') + const buildDirectory = dirname(extensionOutputPath) return fileServerMiddleware(request, response, next, { filePath: joinPath(buildDirectory, assetPath),