@@ -10,7 +10,7 @@ import {AbortError, AbortSilentError} from '@shopify/cli-kit/node/error'
1010import lockfile from 'proper-lockfile'
1111import { dirname , joinPath } from '@shopify/cli-kit/node/path'
1212import { outputDebug } from '@shopify/cli-kit/node/output'
13- import { readFile , touchFile , writeFile , fileExistsSync } from '@shopify/cli-kit/node/fs'
13+ import { copyFile , readFile , touchFile , writeFile , fileExistsSync } from '@shopify/cli-kit/node/fs'
1414import { Writable } from 'stream'
1515
1616export interface ExtensionBuildOptions {
@@ -53,6 +53,14 @@ export interface ExtensionBuildOptions {
5353 * The URL where the app is running.
5454 */
5555 appURL ?: string
56+
57+ /**
58+ * When building for a deploy or dev bundle, this is the output path inside the
59+ * bundle directory. When set, build functions write their final artifact here
60+ * instead of extension.outputPath. This avoids mutating extension.outputPath at
61+ * runtime.
62+ */
63+ bundleOutputPath ?: string
5664}
5765
5866/**
@@ -66,12 +74,13 @@ export async function buildUIExtension(extension: ExtensionInstance, options: Ex
6674 env . APP_URL = options . appURL
6775 }
6876
77+ const outputPath = options . bundleOutputPath ?? extension . outputPath
6978 const { main, assets} = extension . getBundleExtensionStdinContent ( )
7079
7180 try {
7281 await bundleExtension ( {
7382 minify : true ,
74- outputPath : extension . outputPath ,
83+ outputPath,
7584 stdin : {
7685 contents : main ,
7786 resolveDir : extension . directory ,
@@ -88,7 +97,7 @@ export async function buildUIExtension(extension: ExtensionInstance, options: Ex
8897 assets . map ( async ( asset ) => {
8998 await bundleExtension ( {
9099 minify : true ,
91- outputPath : joinPath ( dirname ( extension . outputPath ) , asset . outputFileName ) ,
100+ outputPath : joinPath ( dirname ( outputPath ) , asset . outputFileName ) ,
92101 stdin : {
93102 contents : asset . content ,
94103 resolveDir : extension . directory ,
@@ -111,7 +120,7 @@ export async function buildUIExtension(extension: ExtensionInstance, options: Ex
111120
112121 await extension . buildValidation ( )
113122
114- const sizeInfo = await formatBundleSize ( extension . outputPath )
123+ const sizeInfo = await formatBundleSize ( outputPath )
115124 options . stdout . write ( `${ extension . localIdentifier } successfully built${ sizeInfo } ` )
116125}
117126
@@ -140,33 +149,31 @@ export async function buildFunctionExtension(
140149 }
141150
142151 try {
143- const bundlePath = extension . outputPath
144152 const relativeBuildPath =
145153 ( extension as ExtensionInstance < FunctionConfigType > ) . configuration . build ?. path ?? extension . outputRelativePath
146-
147- extension . outputPath = joinPath ( extension . directory , relativeBuildPath )
154+ const buildOutputPath = joinPath ( extension . directory , relativeBuildPath )
148155
149156 if ( extension . isJavaScript ) {
150- await runCommandOrBuildJSFunction ( extension , options )
157+ await runCommandOrBuildJSFunction ( extension , options , buildOutputPath )
151158 } else {
152159 await buildOtherFunction ( extension , options )
153160 }
154161
155162 const wasmOpt = ( extension as ExtensionInstance < FunctionConfigType > ) . configuration . build ?. wasm_opt
156- if ( fileExistsSync ( extension . outputPath ) && wasmOpt ) {
157- await runWasmOpt ( extension . outputPath )
163+ if ( fileExistsSync ( buildOutputPath ) && wasmOpt ) {
164+ await runWasmOpt ( buildOutputPath )
158165 }
159166
160- if ( fileExistsSync ( extension . outputPath ) ) {
161- await runTrampoline ( extension . outputPath )
167+ if ( fileExistsSync ( buildOutputPath ) ) {
168+ await runTrampoline ( buildOutputPath )
162169 }
163170
164- if (
165- fileExistsSync ( extension . outputPath ) &&
166- bundlePath !== extension . outputPath &&
167- dirname ( bundlePath ) !== dirname ( extension . outputPath )
168- ) {
169- await bundleFunctionExtension ( extension . outputPath , bundlePath )
171+ // When building for a bundle, copy + base64-encode into the bundle directory.
172+ // This mirrors how buildUIExtension writes directly to bundleOutputPath via esbuild.
173+ if ( options . bundleOutputPath && fileExistsSync ( buildOutputPath ) ) {
174+ await touchFile ( options . bundleOutputPath )
175+ await copyFile ( buildOutputPath , options . bundleOutputPath )
176+ await bundleFunctionExtension ( options . bundleOutputPath )
170177 }
171178 // eslint-disable-next-line @typescript-eslint/no-explicit-any
172179 } catch ( error : any ) {
@@ -188,21 +195,24 @@ export async function buildFunctionExtension(
188195 }
189196}
190197
191- export async function bundleFunctionExtension ( wasmPath : string , bundlePath : string ) {
192- outputDebug ( `Converting WASM from ${ wasmPath } to base64 in ${ bundlePath } ` )
198+ export async function bundleFunctionExtension ( wasmPath : string ) {
199+ outputDebug ( `Converting WASM to base64 in ${ wasmPath } ` )
193200 const base64Contents = await readFile ( wasmPath , { encoding : 'base64' } )
194- await touchFile ( bundlePath )
195- await writeFile ( bundlePath , base64Contents )
201+ await writeFile ( wasmPath , base64Contents )
196202}
197203
198- async function runCommandOrBuildJSFunction ( extension : ExtensionInstance , options : BuildFunctionExtensionOptions ) {
204+ async function runCommandOrBuildJSFunction (
205+ extension : ExtensionInstance ,
206+ options : BuildFunctionExtensionOptions ,
207+ buildOutputPath : string ,
208+ ) {
199209 if ( extension . buildCommand ) {
200210 if ( extension . typegenCommand ) {
201211 await buildGraphqlTypes ( extension , options )
202212 }
203213 return runCommand ( extension . buildCommand , extension , options )
204214 } else {
205- return buildJSFunction ( extension as ExtensionInstance < FunctionConfigType > , options )
215+ return buildJSFunction ( extension as ExtensionInstance < FunctionConfigType > , options , buildOutputPath )
206216 }
207217}
208218
0 commit comments