diff --git a/packages/app/src/cli/services/function/build.test.ts b/packages/app/src/cli/services/function/build.test.ts index b1ca9f3f7e..677900c336 100644 --- a/packages/app/src/cli/services/function/build.test.ts +++ b/packages/app/src/cli/services/function/build.test.ts @@ -4,7 +4,7 @@ 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' import {joinPath} from '@shopify/cli-kit/node/path' -import {inTemporaryDirectory, mkdir, writeFile} from '@shopify/cli-kit/node/fs' +import {inTemporaryDirectory, mkdir, writeFile, removeFile} from '@shopify/cli-kit/node/fs' import {build as esBuild} from 'esbuild' vi.mock('@shopify/cli-kit/node/system') @@ -63,6 +63,10 @@ async function installShopifyLibrary(tmpDir: string) { const shopifyFunction = joinPath(shopifyFunctionDir, 'index.ts') await mkdir(shopifyFunctionDir) await writeFile(shopifyFunction, '') + + const runModule = joinPath(shopifyFunctionDir, 'run.ts') + await writeFile(runModule, '') + return shopifyFunction } @@ -112,7 +116,23 @@ describe('bundleExtension', () => { const got = bundleExtension(ourFunction, {stdout, stderr, signal, app}) // Then - await expect(got).rejects.toThrow(/Could not find the Shopify Function runtime/) + await expect(got).rejects.toThrow(/Could not find the Shopify Functions JavaScript library/) + }) + }) + + test('errors if shopify library lacks the run module', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + const ourFunction = await testFunctionExtension({dir: tmpDir}) + ourFunction.entrySourceFilePath = joinPath(tmpDir, 'src/index.ts') + const shopifyFunction = await installShopifyLibrary(tmpDir) + await removeFile(joinPath(shopifyFunction, '..', 'run.ts')) + + // When + const got = bundleExtension(ourFunction, {stdout, stderr, signal, app}) + + // Then + await expect(got).rejects.toThrow(/Could not find the Shopify Functions JavaScript library/) }) }) @@ -173,6 +193,7 @@ describe('ExportJavyBuilder', () => { // Given const ourFunction = await testFunctionExtension({dir: tmpDir}) ourFunction.entrySourceFilePath = joinPath(tmpDir, 'src/index.ts') + const shopifyFunction = await installShopifyLibrary(tmpDir) // When const got = builder.bundle( @@ -210,6 +231,7 @@ describe('ExportJavyBuilder', () => { await inTemporaryDirectory(async (tmpDir) => { // Given const ourFunction = await testFunctionExtension({dir: tmpDir}) + const shopifyFunction = await installShopifyLibrary(tmpDir) // When const got = builder.bundle(ourFunction, {stdout, stderr, signal, app}) diff --git a/packages/app/src/cli/services/function/build.ts b/packages/app/src/cli/services/function/build.ts index 4990f85393..ed35eb1aa1 100644 --- a/packages/app/src/cli/services/function/build.ts +++ b/packages/app/src/cli/services/function/build.ts @@ -5,7 +5,7 @@ import {FunctionConfigType} from '../../models/extensions/specifications/functio import {AppInterface} from '../../models/app/app.js' import {EsbuildEnvVarRegex} from '../../constants.js' import {hyphenate, camelize} from '@shopify/cli-kit/common/string' -import {outputDebug} from '@shopify/cli-kit/node/output' +import {outputContent, outputDebug, outputToken} from '@shopify/cli-kit/node/output' import {exec} from '@shopify/cli-kit/node/system' import {joinPath} from '@shopify/cli-kit/node/path' import {build as esBuild, BuildResult} from 'esbuild' @@ -14,6 +14,7 @@ import {AbortSignal} from '@shopify/cli-kit/node/abort' import {renderTasks} from '@shopify/cli-kit/node/ui' import {pickBy} from '@shopify/cli-kit/common/object' import {runWithTimer} from '@shopify/cli-kit/node/metadata' +import {AbortError} from '@shopify/cli-kit/node/error' import {Writable} from 'stream' interface JSFunctionBuildOptions { @@ -104,24 +105,47 @@ export async function buildGraphqlTypes( }) } -export async function bundleExtension( - fun: ExtensionInstance, - options: JSFunctionBuildOptions, - processEnv = process.env, -) { +async function checkForShopifyFunctionRuntimeEntrypoint(fun: ExtensionInstance) { const entryPoint = await findPathUp('node_modules/@shopify/shopify_function/index.ts', { type: 'file', cwd: fun.directory, }) - if (!entryPoint) { - throw new Error( - "Could not find the Shopify Function runtime. Make sure you have '@shopify/shopify_function' installed", + + const runModule = await findPathUp('node_modules/@shopify/shopify_function/run.ts', { + type: 'file', + cwd: fun.directory, + }) + + if (!entryPoint || !runModule) { + throw new AbortError( + 'Could not find the Shopify Functions JavaScript library.', + outputContent`Make sure you have the latest ${outputToken.yellow( + '@shopify/shopify_function', + )} library installed.`, + [ + outputContent`Add ${outputToken.green( + '"@shopify/shopify_function": "1.0.0"', + )} to the dependencies section of the package.json file in your function's directory, if not already present.` + .value, + `Run your package manager's install command to update dependencies.`, + ], ) } + if (!fun.entrySourceFilePath) { - throw new Error('Could not find your function entry point. It must be in src/index.js or src/index.ts') + throw new AbortError('Could not find your function entry point. It must be in src/index.js or src/index.ts') } + return entryPoint +} + +export async function bundleExtension( + fun: ExtensionInstance, + options: JSFunctionBuildOptions, + processEnv = process.env, +) { + const entryPoint = await checkForShopifyFunctionRuntimeEntrypoint(fun) + const esbuildOptions = { ...getESBuildOptions(fun.directory, fun.entrySourceFilePath, options.app.dotenv?.variables ?? {}, processEnv), entryPoints: [entryPoint], @@ -227,9 +251,7 @@ export class ExportJavyBuilder implements JavyBuilder { } async bundle(fun: ExtensionInstance, options: JSFunctionBuildOptions, processEnv = process.env) { - if (!fun.entrySourceFilePath) { - throw new Error('Could not find your function entry point. It must be in src/index.js or src/index.ts') - } + await checkForShopifyFunctionRuntimeEntrypoint(fun) const contents = this.entrypointContents outputDebug('Generating dist/function.js using generated module:')