diff --git a/.changeset/tough-guests-behave.md b/.changeset/tough-guests-behave.md new file mode 100644 index 00000000000..70bfc102ca1 --- /dev/null +++ b/.changeset/tough-guests-behave.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Validate the @shopify/shopify_function NPM package version is compatible with the Javy version diff --git a/packages/app/src/cli/services/function/build.test.ts b/packages/app/src/cli/services/function/build.test.ts index af07517f4b2..463f926927c 100644 --- a/packages/app/src/cli/services/function/build.test.ts +++ b/packages/app/src/cli/services/function/build.test.ts @@ -67,6 +67,9 @@ async function installShopifyLibrary(tmpDir: string) { const runModule = joinPath(shopifyFunctionDir, 'run.ts') await writeFile(runModule, '') + const packageJson = joinPath(shopifyFunctionDir, 'package.json') + await writeFile(packageJson, JSON.stringify({version: '1.0.0'})) + return shopifyFunction } @@ -136,6 +139,28 @@ describe('bundleExtension', () => { }) }) + test('errors if shopify library is not on a compatible version', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + const incompatibleVersion = '0.0.1' + const ourFunction = await testFunctionExtension({dir: tmpDir}) + ourFunction.entrySourceFilePath = joinPath(tmpDir, 'src/index.ts') + await installShopifyLibrary(tmpDir) + await writeFile( + joinPath(tmpDir, 'node_modules/@shopify/shopify_function/package.json'), + JSON.stringify({version: incompatibleVersion}), + ) + + // When + const got = bundleExtension(ourFunction, {stdout, stderr, signal, app}) + + // Then + await expect(got).rejects.toThrow( + /The installed version of the Shopify Functions JavaScript library is not compatible with this version of Shopify CLI./, + ) + }) + }) + test('errors if user function not found', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given diff --git a/packages/app/src/cli/services/function/build.ts b/packages/app/src/cli/services/function/build.ts index 8d86c1ba643..09975277432 100644 --- a/packages/app/src/cli/services/function/build.ts +++ b/packages/app/src/cli/services/function/build.ts @@ -9,7 +9,7 @@ import {outputContent, outputDebug, outputToken} from '@shopify/cli-kit/node/out import {exec} from '@shopify/cli-kit/node/system' import {dirname, joinPath} from '@shopify/cli-kit/node/path' import {build as esBuild, BuildResult} from 'esbuild' -import {findPathUp, inTemporaryDirectory, writeFile} from '@shopify/cli-kit/node/fs' +import {findPathUp, inTemporaryDirectory, readFile, writeFile} from '@shopify/cli-kit/node/fs' import {AbortSignal} from '@shopify/cli-kit/node/abort' import {renderTasks} from '@shopify/cli-kit/node/ui' import {pickBy} from '@shopify/cli-kit/common/object' @@ -17,6 +17,26 @@ import {runWithTimer} from '@shopify/cli-kit/node/metadata' import {AbortError} from '@shopify/cli-kit/node/error' import {Writable} from 'stream' +export const SHOPIFY_FUNCTION_NPM_PACKAGE_MAJOR_VERSION = '1' + +class InvalidShopifyFunctionPackageError extends AbortError { + constructor(message: string) { + super( + message, + outputContent`Make sure you have a compatible version of the ${outputToken.yellow( + '@shopify/shopify_function', + )} library installed.`, + [ + outputContent`Add ${outputToken.green( + `"@shopify/shopify_function": "~${SHOPIFY_FUNCTION_NPM_PACKAGE_MAJOR_VERSION}.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.`, + ], + ) + } +} + interface JSFunctionBuildOptions { stdout: Writable stderr: Writable @@ -117,19 +137,7 @@ async function checkForShopifyFunctionRuntimeEntrypoint(fun: ExtensionInstance) { + const packageJsonPath = await findPathUp('node_modules/@shopify/shopify_function/package.json', { + type: 'file', + cwd: fun.directory, + }) + + if (!packageJsonPath) { + throw new InvalidShopifyFunctionPackageError('Could not find the Shopify Functions JavaScript library.') + } + + const packageJson = JSON.parse(await readFile(packageJsonPath)) + const majorVersion = packageJson.version.split('.')[0] + + if (majorVersion !== SHOPIFY_FUNCTION_NPM_PACKAGE_MAJOR_VERSION) { + throw new InvalidShopifyFunctionPackageError( + 'The installed version of the Shopify Functions JavaScript library is not compatible with this version of Shopify CLI.', + ) + } +} + export async function bundleExtension( fun: ExtensionInstance, options: JSFunctionBuildOptions, processEnv = process.env, ) { + await validateShopifyFunctionPackageVersion(fun) const entryPoint = await checkForShopifyFunctionRuntimeEntrypoint(fun) const esbuildOptions = { @@ -276,6 +305,7 @@ export class ExportJavyBuilder implements JavyBuilder { } async bundle(fun: ExtensionInstance, options: JSFunctionBuildOptions, processEnv = process.env) { + await validateShopifyFunctionPackageVersion(fun) await checkForShopifyFunctionRuntimeEntrypoint(fun) const contents = this.entrypointContents diff --git a/packages/app/src/cli/services/generate/extension.ts b/packages/app/src/cli/services/generate/extension.ts index 5afacabf85f..de4c156bb43 100644 --- a/packages/app/src/cli/services/generate/extension.ts +++ b/packages/app/src/cli/services/generate/extension.ts @@ -1,6 +1,6 @@ import {configurationFileNames, versions} from '../../constants.js' import {AppInterface} from '../../models/app/app.js' -import {buildGraphqlTypes} from '../function/build.js' +import {buildGraphqlTypes, SHOPIFY_FUNCTION_NPM_PACKAGE_MAJOR_VERSION} from '../function/build.js' import {GenerateExtensionContentOutput} from '../../prompts/generate/extension.js' import {ExtensionFlavor, ExtensionTemplate} from '../../models/app/template.js' import {ensureDownloadedExtensionFlavorExists, ensureExtensionDirectoryExists} from '../extensions/common.js' @@ -299,7 +299,10 @@ function getSrcFileExtension(extensionFlavor: ExtensionFlavorValue): SrcFileExte export function getFunctionRuntimeDependencies(templateLanguage: string): DependencyVersion[] { const dependencies: DependencyVersion[] = [] if (templateLanguage === 'javascript') { - dependencies.push({name: '@shopify/shopify_function', version: '1.0.0'}) + dependencies.push({ + name: '@shopify/shopify_function', + version: `~${SHOPIFY_FUNCTION_NPM_PACKAGE_MAJOR_VERSION}.0.0`, + }) } return dependencies }