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 5020cf64359..c765e6e6f6c 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.test.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.test.ts @@ -32,7 +32,6 @@ vi.mock('../../services/build/extension.js', async () => { return { ...actual, buildUIExtension: vi.fn(), - buildThemeExtension: vi.fn(), buildFunctionExtension: vi.fn(), } }) @@ -148,8 +147,16 @@ describe('build', async () => { // Given const extensionInstance = await testTaxCalculationExtension(tmpDir) const options: ExtensionBuildOptions = { - stdout: new Writable(), - stderr: new Writable(), + stdout: new Writable({ + write(chunk, enc, cb) { + cb() + }, + }), + stderr: new Writable({ + write(chunk, enc, cb) { + cb() + }, + }), app: testApp(), environment: 'production', } diff --git a/packages/app/src/cli/models/extensions/extension-instance.ts b/packages/app/src/cli/models/extensions/extension-instance.ts index c6c2ab48eb8..bf90ce1f6b5 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.ts @@ -2,24 +2,19 @@ import {BaseConfigType, MAX_EXTENSION_HANDLE_LENGTH, MAX_UID_LENGTH} from './sch import {FunctionConfigType} from './specifications/function.js' import {ExtensionFeature, ExtensionSpecification} from './specification.js' import {SingleWebhookSubscriptionType} from './specifications/app_config_webhook_schemas/webhooks_schema.js' -import { - ExtensionBuildOptions, - buildFunctionExtension, - buildThemeExtension, - buildUIExtension, - bundleFunctionExtension, -} from '../../services/build/extension.js' -import {bundleThemeExtension, copyFilesForExtension} from '../../services/extensions/bundle.js' +import {ExtensionBuildOptions, bundleFunctionExtension} from '../../services/build/extension.js' +import {bundleThemeExtension} from '../../services/extensions/bundle.js' import {Identifiers} from '../app/identifiers.js' import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' import {AppConfiguration} from '../app/app.js' import {ApplicationURLs} from '../../services/dev/urls.js' +import {executeStep, BuildContext} from '../../services/build/client-steps.js' 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, 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 {fileExists, moveFile, 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' import { @@ -320,32 +315,24 @@ export class ExtensionInstance { - const mode = this.specification.buildConfig.mode - - switch (mode) { - case 'theme': - await buildThemeExtension(this, options) - return bundleThemeExtension(this, options) - case 'function': - return buildFunctionExtension(this, options) - case 'ui': - await buildUIExtension(this, options) - // Copy static assets after build completes - return this.copyStaticAssets() - case 'tax_calculation': - await touchFile(this.outputPath) - await writeFile(this.outputPath, '(()=>{})();') - break - case 'copy_files': - return copyFilesForExtension( - this, - options, - this.specification.buildConfig.filePatterns, - this.specification.buildConfig.ignoredFilePatterns, - ) - case 'hosted_app_home': - case 'none': - break + const {clientSteps = []} = this.specification + + const context: BuildContext = { + extension: this, + options, + stepResults: new Map(), + } + + const steps = clientSteps.find((lifecycle) => lifecycle.lifecycle === 'deploy')?.steps ?? [] + + for (const step of steps) { + // eslint-disable-next-line no-await-in-loop + const result = await executeStep(step, context) + context.stepResults.set(step.id, result) + + if (!result.success && !step.continueOnError) { + throw new Error(`Build step "${step.name}" failed: ${result.error?.message}`) + } } } diff --git a/packages/app/src/cli/models/extensions/specifications/channel.ts b/packages/app/src/cli/models/extensions/specifications/channel.ts index 44ac2c2150d..abe89c4a4e0 100644 --- a/packages/app/src/cli/models/extensions/specifications/channel.ts +++ b/packages/app/src/cli/models/extensions/specifications/channel.ts @@ -10,6 +10,28 @@ const channelSpecificationSpec = createContractBasedModuleSpecification({ mode: 'copy_files', filePatterns: FILE_EXTENSIONS.map((ext) => joinPath(SUBDIRECTORY_NAME, '**', `*.${ext}`)), }, + clientSteps: [ + { + lifecycle: 'deploy', + steps: [ + { + id: 'copy-files', + name: 'Copy Files', + type: 'include_assets', + config: { + inclusions: [ + { + type: 'pattern', + baseDir: SUBDIRECTORY_NAME, + destination: SUBDIRECTORY_NAME, + include: FILE_EXTENSIONS.map((ext) => `**/*.${ext}`), + }, + ], + }, + }, + ], + }, + ], appModuleFeatures: () => [], }) 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 cbae1290736..6bc9241acb6 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 @@ -19,6 +19,15 @@ const checkoutPostPurchaseSpec = createExtensionSpecification({ buildConfig: {mode: 'ui'}, getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, + clientSteps: [ + { + lifecycle: 'deploy', + steps: [ + {id: 'bundle-ui', name: 'Bundle UI Extension', type: 'bundle_ui', config: {}}, + {id: 'copy-static-assets', name: 'Copy Static Assets', type: 'copy_static_assets', config: {}}, + ], + }, + ], 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 ab37ad6a44f..43d2d159cda 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 @@ -25,6 +25,15 @@ const checkoutSpec = createExtensionSpecification({ appModuleFeatures: (_) => ['ui_preview', 'cart_url', 'esbuild', 'single_js_entry_path', 'generates_source_maps'], buildConfig: {mode: 'ui'}, getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, + clientSteps: [ + { + lifecycle: 'deploy', + steps: [ + {id: 'bundle-ui', name: 'Bundle UI Extension', type: 'bundle_ui', config: {}}, + {id: 'copy-static-assets', name: 'Copy Static Assets', type: 'copy_static_assets', config: {}}, + ], + }, + ], deployConfig: async (config, directory) => { return { extension_points: config.extension_points, diff --git a/packages/app/src/cli/models/extensions/specifications/flow_template.ts b/packages/app/src/cli/models/extensions/specifications/flow_template.ts index 19841b9a4ad..67f3759ad76 100644 --- a/packages/app/src/cli/models/extensions/specifications/flow_template.ts +++ b/packages/app/src/cli/models/extensions/specifications/flow_template.ts @@ -49,7 +49,22 @@ const flowTemplateSpec = createExtensionSpecification({ identifier: 'flow_template', schema: FlowTemplateExtensionSchema, appModuleFeatures: (_) => ['ui_preview'], - buildConfig: {mode: 'copy_files', filePatterns: ['*.flow', '*.json', '*.toml']}, + buildConfig: {mode: 'copy_files', filePatterns: ['**/*.flow', '**/*.json', '**/*.toml']}, + clientSteps: [ + { + lifecycle: 'deploy', + steps: [ + { + id: 'copy-files', + name: 'Copy Files', + type: 'include_assets', + config: { + inclusions: [{type: 'pattern', include: ['**/*.flow', '**/*.json', '**/*.toml']}], + }, + }, + ], + }, + ], deployConfig: async (config, extensionPath) => { return { template_handle: config.handle, diff --git a/packages/app/src/cli/models/extensions/specifications/function.ts b/packages/app/src/cli/models/extensions/specifications/function.ts index a6b8de34f00..9f343ac6611 100644 --- a/packages/app/src/cli/models/extensions/specifications/function.ts +++ b/packages/app/src/cli/models/extensions/specifications/function.ts @@ -90,6 +90,12 @@ const functionSpec = createExtensionSpecification({ appModuleFeatures: (_) => ['function'], buildConfig: {mode: 'function'}, getOutputRelativePath: (_extension: ExtensionInstance) => joinPath('dist', 'index.wasm'), + clientSteps: [ + { + lifecycle: 'deploy', + steps: [{id: 'build-function', name: 'Build Function', type: 'build_function', config: {}}], + }, + ], deployConfig: async (config, directory, apiKey) => { let inputQuery: string | undefined const moduleId = randomUUID() 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 2d9dc37b4ee..d429c5e6846 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 @@ -17,6 +17,15 @@ const posUISpec = createExtensionSpecification({ appModuleFeatures: (_) => ['ui_preview', 'esbuild', 'single_js_entry_path'], buildConfig: {mode: 'ui'}, getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, + clientSteps: [ + { + lifecycle: 'deploy', + steps: [ + {id: 'bundle-ui', name: 'Bundle UI Extension', type: 'bundle_ui', config: {}}, + {id: 'copy-static-assets', name: 'Copy Static Assets', type: 'copy_static_assets', config: {}}, + ], + }, + ], 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 2497a236130..dec7d6a7639 100644 --- a/packages/app/src/cli/models/extensions/specifications/product_subscription.ts +++ b/packages/app/src/cli/models/extensions/specifications/product_subscription.ts @@ -18,6 +18,15 @@ const productSubscriptionSpec = createExtensionSpecification({ appModuleFeatures: (_) => ['ui_preview', 'esbuild', 'single_js_entry_path'], buildConfig: {mode: 'ui'}, getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, + clientSteps: [ + { + lifecycle: 'deploy', + steps: [ + {id: 'bundle-ui', name: 'Bundle UI Extension', type: 'bundle_ui', config: {}}, + {id: 'copy-static-assets', name: 'Copy Static Assets', type: 'copy_static_assets', config: {}}, + ], + }, + ], 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 afa518da726..ea4a012330a 100644 --- a/packages/app/src/cli/models/extensions/specifications/tax_calculation.ts +++ b/packages/app/src/cli/models/extensions/specifications/tax_calculation.ts @@ -34,6 +34,12 @@ const spec = createExtensionSpecification({ buildConfig: {mode: 'tax_calculation'}, getOutputRelativePath: (extension: ExtensionInstance) => joinPath('dist', `${extension.handle}.js`), + clientSteps: [ + { + lifecycle: 'deploy', + steps: [{id: 'create-tax-stub', name: 'Create Tax Stub', type: 'create_tax_stub', config: {}}], + }, + ], deployConfig: async (config, _) => { return { production_api_base_url: config.production_api_base_url, diff --git a/packages/app/src/cli/models/extensions/specifications/theme.ts b/packages/app/src/cli/models/extensions/specifications/theme.ts index b3cc7510d27..c01fa7d5761 100644 --- a/packages/app/src/cli/models/extensions/specifications/theme.ts +++ b/packages/app/src/cli/models/extensions/specifications/theme.ts @@ -13,6 +13,15 @@ const themeSpec = createExtensionSpecification({ partnersWebIdentifier: 'theme_app_extension', graphQLType: 'theme_app_extension', buildConfig: {mode: 'theme'}, + clientSteps: [ + { + lifecycle: 'deploy', + steps: [ + {id: 'build-theme', name: 'Build Theme Extension', type: 'build_theme', config: {}}, + {id: 'bundle-theme', name: 'Bundle Theme Extension', type: 'bundle_theme', config: {}}, + ], + }, + ], appModuleFeatures: (_) => { return ['theme'] }, 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 8f8dc83d02a..5d5a7550f41 100644 --- a/packages/app/src/cli/models/extensions/specifications/ui_extension.ts +++ b/packages/app/src/cli/models/extensions/specifications/ui_extension.ts @@ -104,6 +104,15 @@ const uiExtensionSpec = createExtensionSpecification({ schema: UIExtensionSchema, buildConfig: {mode: 'ui'}, getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, + clientSteps: [ + { + lifecycle: 'deploy', + steps: [ + {id: 'bundle-ui', name: 'Bundle UI Extension', type: 'bundle_ui', config: {}}, + {id: 'copy-static-assets', name: 'Copy Static Assets', type: 'copy_static_assets', config: {}}, + ], + }, + ], 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 e512e66e5f4..66fcd908375 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 @@ -35,6 +35,15 @@ const webPixelSpec = createExtensionSpecification({ appModuleFeatures: (_) => ['esbuild', 'single_js_entry_path'], buildConfig: {mode: 'ui'}, getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, + clientSteps: [ + { + lifecycle: 'deploy', + steps: [ + {id: 'bundle-ui', name: 'Bundle UI Extension', type: 'bundle_ui', config: {}}, + {id: 'copy-static-assets', name: 'Copy Static Assets', type: 'copy_static_assets', config: {}}, + ], + }, + ], deployConfig: async (config, _) => { return { runtime_context: config.runtime_context, diff --git a/packages/app/src/cli/services/build/extension.ts b/packages/app/src/cli/services/build/extension.ts index a8a4c665b8d..9e056d5ecee 100644 --- a/packages/app/src/cli/services/build/extension.ts +++ b/packages/app/src/cli/services/build/extension.ts @@ -1,4 +1,3 @@ -import {runThemeCheck} from './theme-check.js' import {AppInterface} from '../../models/app/app.js' import {bundleExtension} from '../extensions/bundle.js' import {buildGraphqlTypes, buildJSFunction, runTrampoline, runWasmOpt} from '../function/build.js' @@ -55,16 +54,6 @@ export interface ExtensionBuildOptions { appURL?: string } -/** - * It builds the theme extensions. - * @param options - Build options. - */ -export async function buildThemeExtension(extension: ExtensionInstance, options: ExtensionBuildOptions): Promise { - options.stdout.write(`Running theme check on your Theme app extension...`) - const offenses = await runThemeCheck(extension.directory) - if (offenses) options.stdout.write(offenses) -} - /** * It builds the UI extensions. * @param options - Build options. diff --git a/packages/app/src/cli/services/build/steps/create-tax-stub-step.ts b/packages/app/src/cli/services/build/steps/create-tax-stub-step.ts index 5634f76b867..4975d5bb3eb 100644 --- a/packages/app/src/cli/services/build/steps/create-tax-stub-step.ts +++ b/packages/app/src/cli/services/build/steps/create-tax-stub-step.ts @@ -1,4 +1,4 @@ -import {touchFile, writeFile} from '@shopify/cli-kit/node/fs' +import {touchFile, writeFile, fileExists, isDirectory} from '@shopify/cli-kit/node/fs' import type {LifecycleStep, BuildContext} from '../client-steps.js' /** @@ -9,6 +9,9 @@ import type {LifecycleStep, BuildContext} from '../client-steps.js' */ export async function executeCreateTaxStubStep(_step: LifecycleStep, context: BuildContext): Promise { const {extension} = context + if ((await fileExists(extension.outputPath)) && (await isDirectory(extension.outputPath))) { + throw new Error(`outputPath '${extension.outputPath}' is a directory — expected a file path for the tax stub`) + } await touchFile(extension.outputPath) await writeFile(extension.outputPath, '(()=>{})();') }