From 4a8884f03dc08ecf5be9bc6872b450fc5f5cbcf0 Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Thu, 9 Apr 2026 22:56:55 +0200 Subject: [PATCH] fix: copy raw binary to dist/index.wasm during standalone builds, not base64 --- .../src/cli/services/build/extension.test.ts | 36 ++++++++++++++++--- .../app/src/cli/services/build/extension.ts | 19 ++++++---- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/packages/app/src/cli/services/build/extension.test.ts b/packages/app/src/cli/services/build/extension.test.ts index 7f4be98556..9fe8052cf6 100644 --- a/packages/app/src/cli/services/build/extension.test.ts +++ b/packages/app/src/cli/services/build/extension.test.ts @@ -7,7 +7,7 @@ import {beforeEach, describe, expect, test, vi} from 'vitest' import {exec} from '@shopify/cli-kit/node/system' import lockfile from 'proper-lockfile' import {AbortError} from '@shopify/cli-kit/node/error' -import {fileExistsSync, touchFile, writeFile} from '@shopify/cli-kit/node/fs' +import {copyFile, fileExistsSync, touchFile, writeFile} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' vi.mock('@shopify/cli-kit/node/system') @@ -418,7 +418,32 @@ describe('buildFunctionExtension', () => { expect(runWasmOpt).toHaveBeenCalled() }) - test('does not rebundle when build.path stays in the default output directory', async () => { + test('copies raw binary (not base64) to dist/index.wasm when build.path is in a different dir (standalone build)', async () => { + // Given + extension.configuration.build!.path = 'target/wasm32-wasi/release/my_func.wasm' + vi.mocked(fileExistsSync).mockReturnValue(true) + + // When + await expect( + buildFunctionExtension(extension, { + stdout, + stderr, + signal, + app, + environment: 'production', + }), + ).resolves.toBeUndefined() + + // Then: copyFile is used (raw binary copy), NOT base64 bundling via touchFile/writeFile + expect(copyFile).toHaveBeenCalledWith( + joinPath(extension.directory, 'target/wasm32-wasi/release/my_func.wasm'), + joinPath(extension.directory, 'dist', 'index.wasm'), + ) + expect(touchFile).not.toHaveBeenCalled() + expect(writeFile).not.toHaveBeenCalled() + }) + + test('copies raw binary to dist/index.wasm when build.path stays in the default output directory', async () => { // Given extension.configuration.build!.path = 'dist/custom.wasm' vi.mocked(fileExistsSync).mockReturnValue(true) @@ -434,8 +459,11 @@ describe('buildFunctionExtension', () => { }), ).resolves.toBeUndefined() - // Then - expect(fileExistsSync).toHaveBeenCalledWith(joinPath(extension.directory, 'dist/custom.wasm')) + // Then: copyFile is used (raw binary copy), NOT base64 bundling via touchFile/writeFile + expect(copyFile).toHaveBeenCalledWith( + joinPath(extension.directory, 'dist/custom.wasm'), + joinPath(extension.directory, 'dist', 'index.wasm'), + ) expect(touchFile).not.toHaveBeenCalled() expect(writeFile).not.toHaveBeenCalled() }) diff --git a/packages/app/src/cli/services/build/extension.ts b/packages/app/src/cli/services/build/extension.ts index 97e8eca8f7..155ddbdaf6 100644 --- a/packages/app/src/cli/services/build/extension.ts +++ b/packages/app/src/cli/services/build/extension.ts @@ -10,7 +10,7 @@ import {AbortError, AbortSilentError} from '@shopify/cli-kit/node/error' import lockfile from 'proper-lockfile' import {dirname, joinPath} from '@shopify/cli-kit/node/path' import {outputDebug} from '@shopify/cli-kit/node/output' -import {readFile, touchFile, writeFile, fileExistsSync} from '@shopify/cli-kit/node/fs' +import {copyFile, readFile, touchFile, writeFile, fileExistsSync} from '@shopify/cli-kit/node/fs' import {Writable} from 'stream' export interface ExtensionBuildOptions { @@ -161,12 +161,17 @@ export async function buildFunctionExtension( await runTrampoline(extension.outputPath) } - if ( - fileExistsSync(extension.outputPath) && - bundlePath !== extension.outputPath && - dirname(bundlePath) !== dirname(extension.outputPath) - ) { - await bundleFunctionExtension(extension.outputPath, bundlePath) + if (fileExistsSync(extension.outputPath) && bundlePath !== extension.outputPath) { + const projectOutputPath = joinPath(extension.directory, extension.outputRelativePath) + if (bundlePath === projectOutputPath) { + // Standalone build (e.g. `shopify app build`): copy raw binary to dist/index.wasm so + // vitest and other local tooling can load the wasm directly. + await copyFile(extension.outputPath, bundlePath) + } else if (dirname(bundlePath) !== dirname(extension.outputPath)) { + // Bundle build for deploy: base64-encode into the bundle directory to satisfy the + // server-side contract of uploaded_files["dist/index.wasm"]. + await bundleFunctionExtension(extension.outputPath, bundlePath) + } } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) {