diff --git a/packages/app/src/cli/models/extensions/extension-instance.ts b/packages/app/src/cli/models/extensions/extension-instance.ts index 009c8b8038..36048ab391 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.ts @@ -28,9 +28,9 @@ import {constantize, slugify} from '@shopify/cli-kit/common/string' import {hashString, randomUUID} from '@shopify/cli-kit/node/crypto' import {partnersFqdn} from '@shopify/cli-kit/node/context/fqdn' import {joinPath} from '@shopify/cli-kit/node/path' -import {useThemebundling} from '@shopify/cli-kit/node/context/local' import {fileExists, touchFile, writeFile} from '@shopify/cli-kit/node/fs' import {getPathValue} from '@shopify/cli-kit/common/object' +import {useThemebundling} from '@shopify/cli-kit/node/context/local' export const CONFIG_EXTENSION_IDS = [ AppAccessSpecIdentifier, @@ -205,6 +205,7 @@ export class ExtensionInstance { + // To ensure this is not a breaking change we will delete this upload line on a second pass after we've ensured the new file upload process from core is working. const {moduleId} = await uploadWasmBlob(this.localIdentifier, this.outputPath, developerPlatformClient) return this.specification.deployConfig?.(this.configuration, this.directory, apiKey, moduleId) } @@ -339,7 +340,6 @@ export class ExtensionInstance ['function'], + appModuleFeatures: (_) => ['function', 'bundling'], deployConfig: async (config, directory, apiKey, moduleId) => { let inputQuery: string | undefined const inputQueryPath = joinPath(directory, 'input.graphql') diff --git a/packages/app/src/cli/services/build/extension.ts b/packages/app/src/cli/services/build/extension.ts index bf81e275fb..c35d8f5ad7 100644 --- a/packages/app/src/cli/services/build/extension.ts +++ b/packages/app/src/cli/services/build/extension.ts @@ -10,6 +10,7 @@ import {AbortError, AbortSilentError} from '@shopify/cli-kit/node/error' import lockfile from 'proper-lockfile' import {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 {Writable} from 'stream' export interface ExtensionBuildOptions { @@ -141,11 +142,18 @@ export async function buildFunctionExtension( } try { + const bundlePath = joinPath(extension.outputPath.split('/').slice(0, -2).join('/'), 'index.wasm') + extension.outputPath = joinPath(extension.directory, joinPath('dist', 'function.wasm')) if (extension.isJavaScript) { await runCommandOrBuildJSFunction(extension, options) } else { await buildOtherFunction(extension, options) } + if (fileExistsSync(extension.outputPath)) { + const base64Contents = await readFile(extension.outputPath, {encoding: 'base64'}) + await touchFile(bundlePath) + await writeFile(bundlePath, base64Contents) + } } finally { await releaseLock() } diff --git a/packages/app/src/cli/services/deploy.test.ts b/packages/app/src/cli/services/deploy.test.ts index 162903f1a6..d8b1ade536 100644 --- a/packages/app/src/cli/services/deploy.test.ts +++ b/packages/app/src/cli/services/deploy.test.ts @@ -243,7 +243,7 @@ describe('deploy', () => { expect(updateAppIdentifiers).toHaveBeenCalledOnce() }) - test('uploads the extension bundle with 1 function', async () => { + test('uploads the extension bundle with 1 function extension', async () => { // Given const functionExtension = await testFunctionExtension() vi.spyOn(functionExtension, 'preDeployValidation').mockImplementation(async () => {}) @@ -290,7 +290,7 @@ describe('deploy', () => { ], developerPlatformClient, extensionIds: {}, - bundlePath: undefined, + bundlePath: expect.stringMatching(/bundle.zip$/), release: true, }) expect(bundleAndBuildExtensions).toHaveBeenCalledOnce() diff --git a/packages/app/src/cli/services/deploy/bundle.test.ts b/packages/app/src/cli/services/deploy/bundle.test.ts index 1d62b5256e..d6640b37c7 100644 --- a/packages/app/src/cli/services/deploy/bundle.test.ts +++ b/packages/app/src/cli/services/deploy/bundle.test.ts @@ -1,5 +1,5 @@ import {bundleAndBuildExtensions} from './bundle.js' -import {testApp, testThemeExtensions, testUIExtension} from '../../models/app/app.test-data.js' +import {testApp, testFunctionExtension, testThemeExtensions, testUIExtension} from '../../models/app/app.test-data.js' import {AppInterface} from '../../models/app/app.js' import {describe, expect, test, vi} from 'vitest' import * as file from '@shopify/cli-kit/node/fs' @@ -107,4 +107,35 @@ describe('bundleAndBuildExtensions', () => { await expect(file.fileExists(bundlePath)).resolves.toBeTruthy() }) }) + + test('creates a zip file for a function extension', async () => { + await file.inTemporaryDpackages/app/package.jsonirectory(async (tmpDir: string) => { + // Given + const bundlePath = joinPath(tmpDir, 'bundle.zip') + + const functionExtension = await testFunctionExtension() + const extensionBundleMock = vi.fn().mockImplementation(async (options, bundleDirectory, identifiers) => { + file.writeFileSync(joinPath(bundleDirectory, 'index.wasm'), '') + }) + functionExtension.buildForBundle = extensionBundleMock + const app = testApp({allExtensions: [functionExtension]}) + + const extensions: {[key: string]: string} = {} + for (const extension of app.allExtensions) { + extensions[extension.localIdentifier] = extension.localIdentifier + } + const identifiers = { + app: 'app-id', + extensions, + extensionIds: {}, + extensionsNonUuidManaged: {}, + } + + // When + await bundleAndBuildExtensions({app, identifiers, bundlePath}, {}) + + // Then + await expect(file.fileExists(bundlePath)).resolves.toBeTruthy() + }) + }) }) diff --git a/packages/app/src/cli/services/dev/update-extension.test.ts b/packages/app/src/cli/services/dev/update-extension.test.ts index dd9f7a4837..b062684f66 100644 --- a/packages/app/src/cli/services/dev/update-extension.test.ts +++ b/packages/app/src/cli/services/dev/update-extension.test.ts @@ -1,6 +1,7 @@ import {reloadExtensionConfig, updateExtensionDraft} from './update-extension.js' import { placeholderAppConfiguration, + testFunctionExtension, testDeveloperPlatformClient, testPaymentExtensions, testThemeExtensions, @@ -9,12 +10,14 @@ import { import {parseConfigurationFile, parseConfigurationObjectAgainstSpecification} from '../../models/app/loader.js' import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' import {ExtensionUpdateDraftMutationVariables} from '../../api/graphql/partners/generated/update-draft.js' +import {uploadWasmBlob} from '../deploy/upload.js' import {inTemporaryDirectory, mkdir, writeFile} from '@shopify/cli-kit/node/fs' import {outputInfo} from '@shopify/cli-kit/node/output' import {describe, expect, vi, test} from 'vitest' import {joinPath} from '@shopify/cli-kit/node/path' import {platformAndArch} from '@shopify/cli-kit/node/os' +vi.mock('../deploy/upload.js') vi.mock('@shopify/cli-kit/node/output') vi.mock('../../models/app/loader.js', async () => { const actual: any = await vi.importActual('../../models/app/loader.js') @@ -186,6 +189,48 @@ describe('updateExtensionDraft()', () => { }) }) + test('updates draft successfully for function app extension', async () => { + const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient() + await inTemporaryDirectory(async (tmpDir) => { + const mockExtension = await testFunctionExtension({dir: tmpDir}) + + const filepath = 'index.wasm' + const content = 'test content' + const base64Content = Buffer.from(content).toString('base64') + await mkdir(joinPath(mockExtension.directory, 'dist')) + await writeFile(joinPath(mockExtension.directory, 'dist', filepath), content) + vi.mocked(uploadWasmBlob).mockResolvedValue({url: 'url', moduleId: 'moduleId'}) + + await updateExtensionDraft({ + extension: mockExtension, + developerPlatformClient, + apiKey, + registrationId, + stdout, + stderr, + appConfiguration: placeholderAppConfiguration, + }) + + expect(developerPlatformClient.updateExtension).toHaveBeenCalledWith({ + apiKey, + context: '', + handle: mockExtension.handle, + registrationId, + config: JSON.stringify({ + title: 'test function extension', + module_id: 'moduleId', + description: 'description', + app_key: 'mock-api-key', + api_type: 'product_discounts', + api_version: '2022-07', + enable_creation_ui: true, + localization: {}, + uploaded_files: {'index.wasm': base64Content}, + }), + }) + }) + }) + test('handles user errors with stderr message', async () => { const errorResponse = { extensionUpdateDraft: { diff --git a/packages/app/src/cli/services/dev/update-extension.ts b/packages/app/src/cli/services/dev/update-extension.ts index 2a3308e024..85ba582b8f 100644 --- a/packages/app/src/cli/services/dev/update-extension.ts +++ b/packages/app/src/cli/services/dev/update-extension.ts @@ -50,12 +50,20 @@ export async function updateExtensionDraft({ config = (await extension.deployConfig({apiKey, developerPlatformClient, appConfiguration})) || {} } + let draftableConfig: {[key: string]: unknown} = { + ...config, + serialized_script: encodedFile, + } + if (extension.isFunctionExtension) { + const compiledFiles = await readFile(extension.outputPath, {encoding: 'base64'}) + draftableConfig = { + ...draftableConfig, + uploaded_files: {'index.wasm': compiledFiles}, + } + } const extensionInput: ExtensionUpdateDraftMutationVariables = { apiKey, - config: JSON.stringify({ - ...config, - serialized_script: encodedFile, - }), + config: JSON.stringify(draftableConfig), handle: extension.handle, context: extension.contextValue, registrationId,