diff --git a/.changeset/dry-rice-guess.md b/.changeset/dry-rice-guess.md new file mode 100644 index 0000000000..7002377b8b --- /dev/null +++ b/.changeset/dry-rice-guess.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Update Javy invocation to use Javy plugin diff --git a/packages/app/src/cli/services/function/binaries.test.ts b/packages/app/src/cli/services/function/binaries.test.ts index c354a69a80..7656cd9e2f 100644 --- a/packages/app/src/cli/services/function/binaries.test.ts +++ b/packages/app/src/cli/services/function/binaries.test.ts @@ -1,10 +1,11 @@ -import {javyBinary, functionRunnerBinary, installBinary} from './binaries.js' +import {javyBinary, functionRunnerBinary, downloadBinary, javyPluginBinary} from './binaries.js' import {fetch, Response} from '@shopify/cli-kit/node/http' import {fileExists, removeFile} from '@shopify/cli-kit/node/fs' import {describe, expect, test, vi} from 'vitest' import {gzipSync} from 'zlib' const javy = javyBinary() +const javyPlugin = javyPluginBinary() const functionRunner = functionRunnerBinary() vi.mock('@shopify/cli-kit/node/http', async () => { @@ -117,14 +118,14 @@ describe('javy', () => { }) }) - test('installs Javy', async () => { + test('downloads Javy', async () => { // Given await removeFile(javy.path) await expect(fileExists(javy.path)).resolves.toBeFalsy() vi.mocked(fetch).mockResolvedValue(new Response(gzipSync('javy binary'))) // When - await installBinary(javy) + await downloadBinary(javy) // Then expect(fetch).toHaveBeenCalledOnce() @@ -132,6 +133,36 @@ describe('javy', () => { }) }) +describe('javy-plugin', () => { + test('properties are set correctly', () => { + expect(javyPlugin.name).toBe('javy_quickjs_provider_v3') + expect(javyPlugin.version).match(/^v\d.\d.\d$/) + expect(javyPlugin.path).toMatch(/(\/|\\)javy_quickjs_provider_v3.wasm$/) + }) + + test('downloadUrl returns the correct URL', () => { + // When + const url = javyPlugin.downloadUrl('', '') + + // Then + expect(url).toMatch(/https:\/\/github.com\/bytecodealliance\/javy\/releases\/download\/v\d\.\d\.\d\/plugin.wasm.gz/) + }) + + test('downloads javy-plugin', async () => { + // Given + await removeFile(javyPlugin.path) + await expect(fileExists(javyPlugin.path)).resolves.toBeFalsy() + vi.mocked(fetch).mockResolvedValue(new Response(gzipSync('javy-plugin binary'))) + + // When + await downloadBinary(javyPlugin) + + // Then + expect(fetch).toHaveBeenCalledOnce() + await expect(fileExists(javyPlugin.path)).resolves.toBeTruthy() + }) +}) + describe('functionRunner', () => { test('properties are set correctly', () => { expect(functionRunner.name).toBe('function-runner') @@ -234,14 +265,14 @@ describe('functionRunner', () => { }) }) - test('installs function-runner', async () => { + test('downloads function-runner', async () => { // Given await removeFile(functionRunner.path) await expect(fileExists(functionRunner.path)).resolves.toBeFalsy() vi.mocked(fetch).mockResolvedValue(new Response(gzipSync('function-runner binary'))) // When - await installBinary(functionRunner) + await downloadBinary(functionRunner) // Then expect(fetch).toHaveBeenCalledOnce() diff --git a/packages/app/src/cli/services/function/binaries.ts b/packages/app/src/cli/services/function/binaries.ts index 7e7521af6a..034d28e18f 100644 --- a/packages/app/src/cli/services/function/binaries.ts +++ b/packages/app/src/cli/services/function/binaries.ts @@ -10,12 +10,24 @@ import * as gzip from 'node:zlib' import {fileURLToPath} from 'node:url' const FUNCTION_RUNNER_VERSION = 'v6.3.0' -const JAVY_VERSION = 'v3.2.0' +const JAVY_VERSION = 'v4.0.0' +// The Javy plugin version does not need to match the Javy version. It should +// match the plugin version used in the function-runner version specified above. +const JAVY_PLUGIN_VERSION = 'v3.2.0' + +interface DownloadableBinary { + path: string + name: string + version: string + + downloadUrl(processPlatform: string, processArch: string): string + processResponse(responseStream: PipelineSource, outputStream: fs.WriteStream): Promise +} // The logic for determining the download URL and what to do with the response stream is _coincidentally_ the same for // Javy and function-runner for now. Those methods may not continue to have the same logic in the future. If they -// diverge, make `Binary` an abstract class and create subclasses to handle the different logic polymorphically. -class DownloadableBinary { +// diverge, create different classes to handle the different logic polymorphically. +class Executable implements DownloadableBinary { readonly name: string readonly version: string readonly path: string @@ -71,31 +83,58 @@ class DownloadableBinary { } async processResponse(responseStream: PipelineSource, outputStream: fs.WriteStream): Promise { - const gunzip = gzip.createGunzip() - await stream.pipeline(responseStream, gunzip, outputStream) + return gunzipResponse(responseStream, outputStream) + } +} + +class JavyPlugin implements DownloadableBinary { + readonly name: string + readonly version: string + readonly path: string + + constructor() { + this.name = 'javy_quickjs_provider_v3' + this.version = JAVY_PLUGIN_VERSION + this.path = joinPath(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'javy_quickjs_provider_v3.wasm') + } + + downloadUrl(_processPlatform: string, _processArch: string) { + return `https://github.com/bytecodealliance/javy/releases/download/${this.version}/plugin.wasm.gz` + } + + async processResponse(responseStream: PipelineSource, outputStream: fs.WriteStream): Promise { + return gunzipResponse(responseStream, outputStream) } } let _javy: DownloadableBinary +let _javyPlugin: DownloadableBinary let _functionRunner: DownloadableBinary export function javyBinary() { if (!_javy) { - _javy = new DownloadableBinary('javy', JAVY_VERSION, 'bytecodealliance/javy') + _javy = new Executable('javy', JAVY_VERSION, 'bytecodealliance/javy') } return _javy } +export function javyPluginBinary() { + if (!_javyPlugin) { + _javyPlugin = new JavyPlugin() + } + return _javyPlugin +} + export function functionRunnerBinary() { if (!_functionRunner) { - _functionRunner = new DownloadableBinary('function-runner', FUNCTION_RUNNER_VERSION, 'Shopify/function-runner') + _functionRunner = new Executable('function-runner', FUNCTION_RUNNER_VERSION, 'Shopify/function-runner') } return _functionRunner } -export async function installBinary(bin: DownloadableBinary) { - const isInstalled = await fileExists(bin.path) - if (isInstalled) { +export async function downloadBinary(bin: DownloadableBinary) { + const isDownloaded = await fileExists(bin.path) + if (isDownloaded) { return } @@ -118,7 +157,7 @@ export async function installBinary(bin: DownloadableBinary) { } // Download to a temp location and then move the file only after it's fully processed - // so the `isInstalled` check above will continue to return false if the file hasn't + // so the `isDownloaded` check above will continue to return false if the file hasn't // been fully processed. await inTemporaryDirectory(async (tmpDir) => { const tmpFile = joinPath(tmpDir, 'binary') @@ -132,3 +171,8 @@ export async function installBinary(bin: DownloadableBinary) { 2, ) } + +async function gunzipResponse(responseStream: PipelineSource, outputStream: fs.WriteStream): Promise { + const gunzip = gzip.createGunzip() + await stream.pipeline(responseStream, gunzip, outputStream) +} diff --git a/packages/app/src/cli/services/function/build.test.ts b/packages/app/src/cli/services/function/build.test.ts index 583b060724..b1ca9f3f7e 100644 --- a/packages/app/src/cli/services/function/build.test.ts +++ b/packages/app/src/cli/services/function/build.test.ts @@ -1,5 +1,5 @@ import {buildGraphqlTypes, bundleExtension, runJavy, ExportJavyBuilder, jsExports} from './build.js' -import {javyBinary} from './binaries.js' +import {javyBinary, javyPluginBinary} from './binaries.js' import {testApp, testFunctionExtension} from '../../models/app/app.test-data.js' import {beforeEach, describe, expect, test, vi} from 'vitest' import {exec} from '@shopify/cli-kit/node/system' @@ -143,7 +143,16 @@ describe('runJavy', () => { await expect(got).resolves.toBeUndefined() expect(exec).toHaveBeenCalledWith( javyBinary().path, - ['build', '-C', 'dynamic', '-o', joinPath(ourFunction.directory, 'dist/index.wasm'), 'dist/function.js'], + [ + 'build', + '-C', + 'dynamic', + '-C', + `plugin=${javyPluginBinary().path}`, + '-o', + joinPath(ourFunction.directory, 'dist/index.wasm'), + 'dist/function.js', + ], { cwd: ourFunction.directory, stderr: 'inherit', @@ -230,6 +239,8 @@ describe('ExportJavyBuilder', () => { '-C', 'dynamic', '-C', + `plugin=${javyPluginBinary().path}`, + '-C', expect.stringContaining('wit='), '-C', 'wit-world=shopify-function', diff --git a/packages/app/src/cli/services/function/build.ts b/packages/app/src/cli/services/function/build.ts index 5cd2e9dfa8..59179aeaca 100644 --- a/packages/app/src/cli/services/function/build.ts +++ b/packages/app/src/cli/services/function/build.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import {installBinary, javyBinary} from './binaries.js' +import {downloadBinary, javyBinary, javyPluginBinary} from './binaries.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' import {FunctionConfigType} from '../../models/extensions/specifications/function.js' import {AppInterface} from '../../models/app/app.js' @@ -168,12 +168,23 @@ export async function runJavy( extra: string[] = [], ) { const javy = javyBinary() - await installBinary(javy) + const plugin = javyPluginBinary() + await Promise.all([downloadBinary(javy), downloadBinary(plugin)]) // Using the `build` command we want to emit: // - // `javy build -C dynamic -C wit= -C wit-world=val -o ` - const args = ['build', '-C', 'dynamic', ...extra, '-o', fun.outputPath, 'dist/function.js'] + // `javy build -C dynamic -C plugin=path/to/javy_quickjs_provider_v3.wasm -C wit= -C wit-world=val -o ` + const args = [ + 'build', + '-C', + 'dynamic', + '-C', + `plugin=${plugin.path}`, + ...extra, + '-o', + fun.outputPath, + 'dist/function.js', + ] return exec(javy.path, args, { cwd: fun.directory, @@ -187,7 +198,7 @@ export async function installJavy(app: AppInterface) { const javyRequired = app.allExtensions.some((ext) => ext.features.includes('function') && ext.isJavaScript) if (javyRequired) { const javy = javyBinary() - await installBinary(javy) + await downloadBinary(javy) } } diff --git a/packages/app/src/cli/services/function/runner.test.ts b/packages/app/src/cli/services/function/runner.test.ts index c84cd2202b..2e78d04d54 100644 --- a/packages/app/src/cli/services/function/runner.test.ts +++ b/packages/app/src/cli/services/function/runner.test.ts @@ -1,5 +1,5 @@ import {runFunction} from './runner.js' -import {functionRunnerBinary, installBinary} from './binaries.js' +import {functionRunnerBinary, downloadBinary} from './binaries.js' import {testFunctionExtension} from '../../models/app/app.test-data.js' import {describe, test, vi, expect} from 'vitest' import {exec} from '@shopify/cli-kit/node/system' @@ -10,7 +10,7 @@ vi.mock('./binaries.js', async (importOriginal) => { const original = await importOriginal() return { ...original, - installBinary: vi.fn().mockResolvedValue(undefined), + downloadBinary: vi.fn().mockResolvedValue(undefined), } }) @@ -23,7 +23,7 @@ describe('runFunction', () => { await runFunction({functionExtension}) // Then - expect(installBinary).toHaveBeenCalledOnce() + expect(downloadBinary).toHaveBeenCalledOnce() }) test('runs function with options', async () => { diff --git a/packages/app/src/cli/services/function/runner.ts b/packages/app/src/cli/services/function/runner.ts index 901a0f546d..9ec0e48169 100644 --- a/packages/app/src/cli/services/function/runner.ts +++ b/packages/app/src/cli/services/function/runner.ts @@ -1,4 +1,4 @@ -import {functionRunnerBinary, installBinary} from './binaries.js' +import {functionRunnerBinary, downloadBinary} from './binaries.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' import {FunctionConfigType} from '../../models/extensions/specifications/function.js' import {exec} from '@shopify/cli-kit/node/system' @@ -19,7 +19,7 @@ interface FunctionRunnerOptions { export async function runFunction(options: FunctionRunnerOptions) { const functionRunner = functionRunnerBinary() - await installBinary(functionRunner) + await downloadBinary(functionRunner) const args: string[] = [] if (options.inputPath) {