From 8a10546612e77e6eb77b9f3b7a61f8fbeb550435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Thu, 16 Apr 2026 20:07:25 +0200 Subject: [PATCH] refactor: remove buildConfig from specifications in favor of clientSteps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The buildConfig property on extension specifications was redundant with clientSteps. This removes the BuildConfig type, the property from ExtensionSpecification, and all usages across specification files. Consumer sites in deploy.ts and extension-instance.ts now use clientSteps-based checks (hasDeploySteps getter) instead of buildConfig.mode comparisons. bundleAndBuildExtensions now always creates the bundle directory but only compresses and returns a bundlePath when extensions have deploy steps, avoiding unnecessary GCS uploads for manifest-only bundles. Co-authored-by: Isaac Roldán --- .../models/extensions/extension-instance.ts | 12 ++++-- .../cli/models/extensions/specification.ts | 10 ----- .../models/extensions/specifications/admin.ts | 4 -- .../extensions/specifications/channel.ts | 5 --- .../specifications/checkout_post_purchase.ts | 1 - .../specifications/checkout_ui_extension.ts | 1 - .../specifications/flow_template.ts | 1 - .../extensions/specifications/function.ts | 1 - .../specifications/pos_ui_extension.ts | 1 - .../specifications/product_subscription.ts | 1 - .../specifications/tax_calculation.ts | 1 - .../models/extensions/specifications/theme.ts | 1 - .../extensions/specifications/ui_extension.ts | 1 - .../specifications/web_pixel_extension.ts | 1 - packages/app/src/cli/services/deploy.test.ts | 6 +++ packages/app/src/cli/services/deploy.ts | 17 ++++---- .../src/cli/services/deploy/bundle.test.ts | 42 ++++++++++++++++++- .../app/src/cli/services/deploy/bundle.ts | 17 ++++++-- 18 files changed, 77 insertions(+), 46 deletions(-) diff --git a/packages/app/src/cli/models/extensions/extension-instance.ts b/packages/app/src/cli/models/extensions/extension-instance.ts index b68a22720cd..120e5b1fe47 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.ts @@ -112,6 +112,12 @@ export class ExtensionInstance group.lifecycle === 'deploy' && group.steps.length > 0) ?? false + ) + } + get features(): ExtensionFeature[] { return this.specification.appModuleFeatures(this.configuration) } @@ -340,16 +346,14 @@ export class ExtensionInstance) => string @@ -220,7 +215,6 @@ export function createExtensionSpecification clientSteps?: ClientSteps - buildConfig?: BuildConfig appModuleFeatures?: (config?: TConfiguration) => ExtensionFeature[] transformConfig: TransformationConfig | CustomTransformationConfig uidStrategy?: UidStrategy @@ -288,7 +281,6 @@ export function createConfigExtensionSpecification, | 'identifier' | 'appModuleFeatures' - | 'buildConfig' | 'uidStrategy' | 'clientSteps' | 'experience' @@ -312,7 +303,6 @@ export function createContractBasedModuleSpecification, appModuleFeatures: spec.appModuleFeatures, experience: spec.experience, - buildConfig: spec.buildConfig ?? {mode: 'none'}, clientSteps: spec.clientSteps, uidStrategy: spec.uidStrategy, transformRemoteToLocal: spec.transformRemoteToLocal, diff --git a/packages/app/src/cli/models/extensions/specifications/admin.ts b/packages/app/src/cli/models/extensions/specifications/admin.ts index bdd20cb2131..7aad744d238 100644 --- a/packages/app/src/cli/models/extensions/specifications/admin.ts +++ b/packages/app/src/cli/models/extensions/specifications/admin.ts @@ -41,10 +41,6 @@ const adminSpecificationSpec = createExtensionSpecification({ }, } }, - buildConfig: { - mode: 'copy_files', - filePatterns: [], - }, clientSteps: [ { lifecycle: 'deploy', diff --git a/packages/app/src/cli/models/extensions/specifications/channel.ts b/packages/app/src/cli/models/extensions/specifications/channel.ts index fc3ca3b5a9e..3973fa91d26 100644 --- a/packages/app/src/cli/models/extensions/specifications/channel.ts +++ b/packages/app/src/cli/models/extensions/specifications/channel.ts @@ -1,5 +1,4 @@ import {createContractBasedModuleSpecification} from '../specification.js' -import {joinPath} from '@shopify/cli-kit/node/path' const SUBDIRECTORY_NAME = 'specifications' const FILE_EXTENSIONS = ['json', 'toml', 'yaml', 'yml', 'svg'] @@ -8,10 +7,6 @@ const channelSpecificationSpec = createContractBasedModuleSpecification({ identifier: 'channel_config', uidStrategy: 'single', experience: 'extension', - buildConfig: { - mode: 'copy_files', - filePatterns: FILE_EXTENSIONS.map((ext) => joinPath(SUBDIRECTORY_NAME, '**', `*.${ext}`)), - }, clientSteps: [ { lifecycle: 'deploy', 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 cfc32f4582e..fad34726ed6 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 @@ -16,7 +16,6 @@ const checkoutPostPurchaseSpec = createExtensionSpecification({ partnersWebIdentifier: 'post_purchase', schema: CheckoutPostPurchaseSchema, appModuleFeatures: (_) => ['ui_preview', 'cart_url', 'esbuild', 'single_js_entry_path'], - buildConfig: {mode: 'ui'}, getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, clientSteps: [ 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 bd6715fa762..239f164c9b2 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 @@ -23,7 +23,6 @@ const checkoutSpec = createExtensionSpecification({ dependency, 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`, clientSteps: [ { 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 67f3759ad76..805f953c98d 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,6 @@ const flowTemplateSpec = createExtensionSpecification({ identifier: 'flow_template', schema: FlowTemplateExtensionSchema, appModuleFeatures: (_) => ['ui_preview'], - buildConfig: {mode: 'copy_files', filePatterns: ['**/*.flow', '**/*.json', '**/*.toml']}, clientSteps: [ { lifecycle: 'deploy', diff --git a/packages/app/src/cli/models/extensions/specifications/function.ts b/packages/app/src/cli/models/extensions/specifications/function.ts index d40d83e523b..2d73e819d01 100644 --- a/packages/app/src/cli/models/extensions/specifications/function.ts +++ b/packages/app/src/cli/models/extensions/specifications/function.ts @@ -88,7 +88,6 @@ const functionSpec = createExtensionSpecification({ ], schema: FunctionExtensionSchema, appModuleFeatures: (_) => ['function'], - buildConfig: {mode: 'function'}, getOutputRelativePath: (_extension: ExtensionInstance) => joinPath('dist', 'index.wasm'), devSessionWatchConfig: (extension: ExtensionInstance) => { const config = extension.configuration 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 05134ab44d8..3835d873ec7 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 @@ -15,7 +15,6 @@ const posUISpec = createExtensionSpecification({ dependency, schema: PosUISchema, appModuleFeatures: (_) => ['ui_preview', 'esbuild', 'single_js_entry_path'], - buildConfig: {mode: 'ui'}, getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, clientSteps: [ { 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 6efb44d4a48..77c95fde10f 100644 --- a/packages/app/src/cli/models/extensions/specifications/product_subscription.ts +++ b/packages/app/src/cli/models/extensions/specifications/product_subscription.ts @@ -16,7 +16,6 @@ const productSubscriptionSpec = createExtensionSpecification({ graphQLType: 'subscription_management', schema: BaseSchema, appModuleFeatures: (_) => ['ui_preview', 'esbuild', 'single_js_entry_path'], - buildConfig: {mode: 'ui'}, getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, clientSteps: [ { 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 ea4a012330a..1cc2caa3517 100644 --- a/packages/app/src/cli/models/extensions/specifications/tax_calculation.ts +++ b/packages/app/src/cli/models/extensions/specifications/tax_calculation.ts @@ -31,7 +31,6 @@ const spec = createExtensionSpecification({ identifier: 'tax_calculation', schema: TaxCalculationsSchema, appModuleFeatures: (_) => [], - buildConfig: {mode: 'tax_calculation'}, getOutputRelativePath: (extension: ExtensionInstance) => joinPath('dist', `${extension.handle}.js`), clientSteps: [ diff --git a/packages/app/src/cli/models/extensions/specifications/theme.ts b/packages/app/src/cli/models/extensions/specifications/theme.ts index c01fa7d5761..8e6d27d27e4 100644 --- a/packages/app/src/cli/models/extensions/specifications/theme.ts +++ b/packages/app/src/cli/models/extensions/specifications/theme.ts @@ -12,7 +12,6 @@ const themeSpec = createExtensionSpecification({ schema: BaseSchema, partnersWebIdentifier: 'theme_app_extension', graphQLType: 'theme_app_extension', - buildConfig: {mode: 'theme'}, clientSteps: [ { lifecycle: 'deploy', 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 2656bfb5a16..55377653b4a 100644 --- a/packages/app/src/cli/models/extensions/specifications/ui_extension.ts +++ b/packages/app/src/cli/models/extensions/specifications/ui_extension.ts @@ -83,7 +83,6 @@ const uiExtensionSpec = createExtensionSpecification({ identifier: 'ui_extension', dependency, schema: UIExtensionSchema, - buildConfig: {mode: 'ui'}, getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, clientSteps: [ { 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 903fbaa8288..a6547c226cd 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 @@ -33,7 +33,6 @@ const webPixelSpec = createExtensionSpecification({ partnersWebIdentifier: 'web_pixel', schema: WebPixelSchema, appModuleFeatures: (_) => ['esbuild', 'single_js_entry_path'], - buildConfig: {mode: 'ui'}, getOutputRelativePath: (extension: ExtensionInstance) => `dist/${extension.handle}.js`, clientSteps: [ { diff --git a/packages/app/src/cli/services/deploy.test.ts b/packages/app/src/cli/services/deploy.test.ts index 8a695e91e60..4807b4ebdd2 100644 --- a/packages/app/src/cli/services/deploy.test.ts +++ b/packages/app/src/cli/services/deploy.test.ts @@ -79,6 +79,12 @@ beforeEach(() => { // Mock filterOutImportedExtensions to return the extensions by default vi.mocked(filterOutImportedExtensions).mockImplementation((_app, extensions) => extensions) + + // Mirror real bundleAndBuildExtensions behavior: return the bundlePath when + // at least one extension has deploy steps, otherwise undefined. + vi.mocked(bundleAndBuildExtensions).mockImplementation(async (opts) => + opts.app.allExtensions.some((ext) => ext.hasDeploySteps) ? opts.bundlePath : undefined, + ) }) describe('deploy', () => { diff --git a/packages/app/src/cli/services/deploy.ts b/packages/app/src/cli/services/deploy.ts index 66848fa8666..2f9d31a27ba 100644 --- a/packages/app/src/cli/services/deploy.ts +++ b/packages/app/src/cli/services/deploy.ts @@ -213,20 +213,19 @@ export async function deploy(options: DeployOptions) { let uploadExtensionsBundleResult!: UploadExtensionsBundleOutput try { - const bundle = app.allExtensions.some((ext) => ext.specification.buildConfig.mode !== 'none') - let bundlePath: string | undefined - - if (bundle) { - bundlePath = joinPath(options.app.directory, '.shopify', `deploy-bundle.${developerPlatformClient.bundleFormat}`) - await mkdir(dirname(bundlePath)) - } + const candidateBundlePath = joinPath( + options.app.directory, + '.shopify', + `deploy-bundle.${developerPlatformClient.bundleFormat}`, + ) + await mkdir(dirname(candidateBundlePath)) const appManifest = await app.manifest(identifiers) - await bundleAndBuildExtensions({ + const bundlePath = await bundleAndBuildExtensions({ app, appManifest, - bundlePath, + bundlePath: candidateBundlePath, identifiers, skipBuild: options.skipBuild, isDevDashboardApp: developerPlatformClient.supportsAtomicDeployments, diff --git a/packages/app/src/cli/services/deploy/bundle.test.ts b/packages/app/src/cli/services/deploy/bundle.test.ts index e30d3e091fc..2ec28f886b7 100644 --- a/packages/app/src/cli/services/deploy/bundle.test.ts +++ b/packages/app/src/cli/services/deploy/bundle.test.ts @@ -1,5 +1,11 @@ import {bundleAndBuildExtensions} from './bundle.js' -import {testApp, testFunctionExtension, testThemeExtensions, testUIExtension} from '../../models/app/app.test-data.js' +import { + testApp, + testAppConfigExtensions, + testFunctionExtension, + testThemeExtensions, + testUIExtension, +} from '../../models/app/app.test-data.js' import {AppInterface, AppManifest, WebType} from '../../models/app/app.js' import * as bundle from '../bundle.js' import * as functionBuild from '../function/build.js' @@ -477,4 +483,38 @@ describe('bundleAndBuildExtensions', () => { await expect(file.fileExists(bundlePath)).resolves.toBeTruthy() }) }) + + test('returns undefined and skips compression when no extension has deploy steps', async () => { + await file.inTemporaryDirectory(async (tmpDir: string) => { + // Given + const bundlePath = joinPath(tmpDir, '.shopify', 'deploy-bundle.zip') + const compressSpy = vi.spyOn(bundle, 'compressBundle').mockResolvedValue() + + const configExtension = await testAppConfigExtensions() + const app = testApp({allExtensions: [configExtension], directory: tmpDir}) + + const identifiers = { + app: 'app-id', + extensions: {}, + extensionIds: {}, + extensionsNonUuidManaged: {[configExtension.localIdentifier]: configExtension.localIdentifier}, + } + appManifest = await app.manifest(identifiers) + + // When + const result = await bundleAndBuildExtensions({ + app, + appManifest, + identifiers, + bundlePath, + skipBuild: false, + isDevDashboardApp: false, + }) + + // Then + expect(result).toBeUndefined() + expect(compressSpy).not.toHaveBeenCalled() + await expect(file.fileExists(bundlePath)).resolves.toBe(false) + }) + }) }) diff --git a/packages/app/src/cli/services/deploy/bundle.ts b/packages/app/src/cli/services/deploy/bundle.ts index 5811ff368a9..216645b51a0 100644 --- a/packages/app/src/cli/services/deploy/bundle.ts +++ b/packages/app/src/cli/services/deploy/bundle.ts @@ -12,13 +12,19 @@ import {Writable} from 'stream' interface BundleOptions { app: AppInterface appManifest: AppManifest - bundlePath?: string + bundlePath: string identifiers?: Identifiers skipBuild: boolean isDevDashboardApp: boolean } -export async function bundleAndBuildExtensions(options: BundleOptions) { +/** + * Builds all extensions into a bundle directory and compresses it when at + * least one extension declares deploy steps. Returns the bundlePath in that + * case, or undefined when no extension has deploy steps and there's nothing + * to upload beyond the manifest. + */ +export async function bundleAndBuildExtensions(options: BundleOptions): Promise { const bundleDirectory = joinPath(options.app.directory, '.shopify', 'deploy-bundle') await rmdir(bundleDirectory, {force: true}) await mkdir(bundleDirectory) @@ -73,7 +79,12 @@ export async function bundleAndBuildExtensions(options: BundleOptions) { showTimestamps: false, }) - if (options.bundlePath) { + const hasExtensionOutput = options.app.allExtensions.some((ext) => ext.hasDeploySteps) + + if (hasExtensionOutput) { await compressBundle(bundleDirectory, options.bundlePath) + return options.bundlePath } + + return undefined }