diff --git a/packages/app/src/cli/models/extensions/specifications/marketing_activity_schemas/marketing_activity_schema.test.ts b/packages/app/src/cli/models/extensions/specifications/marketing_activity_schemas/marketing_activity_schema.test.ts deleted file mode 100644 index cddd66aaf30..00000000000 --- a/packages/app/src/cli/models/extensions/specifications/marketing_activity_schemas/marketing_activity_schema.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import {MarketingActivityExtensionSchema} from './marketing_activity_schema.js' -import {describe, expect, test} from 'vitest' -import {zod} from '@shopify/cli-kit/node/schema' - -describe('MarketingActivityExtensionSchema', () => { - const config = { - name: 'test extension', - type: 'marketing_activity_extension', - title: 'test extension 123', - description: 'test description 123', - api_path: '/api', - tactic: 'ad', - marketing_channel: 'social', - referring_domain: 'http://foo.bar', - is_automation: false, - use_external_editor: false, - preview_data: { - types: [ - { - label: 'mobile', - value: 'http://foo.bar/preview', - }, - ], - }, - fields: [ - { - id: '123', - ui_type: 'text-single-line', - name: 'test_field', - label: 'test field', - help_text: 'help text', - required: false, - min_length: 1, - max_length: 50, - placeholder: 'placeholder', - }, - ], - } - - test('validates a configuration with valid fields', async () => { - // When - const {success} = MarketingActivityExtensionSchema.safeParse(config) - - // Then - expect(success).toBe(true) - }) - - describe('fields', () => { - test('throws an error if a field is not an object', async () => { - // When/Then - expect(() => - MarketingActivityExtensionSchema.parse({ - ...config, - fields: ['not an object'], - }), - ).toThrowError( - new zod.ZodError([ - { - message: 'Field must be an object', - code: zod.ZodIssueCode.custom, - path: ['fields', 0], - }, - ]), - ) - }) - - test('throws an error if no fields are defined', async () => { - // When/Then - expect(() => - MarketingActivityExtensionSchema.parse({ - ...config, - fields: [], - }), - ).toThrowError( - new zod.ZodError([ - { - code: zod.ZodIssueCode.too_small, - minimum: 1, - type: 'array', - inclusive: true, - exact: false, - message: 'Array must contain at least 1 element(s)', - path: ['fields'], - }, - ]), - ) - }) - - test('throws an error if field does not have ui_type', async () => { - // When/Then - expect(() => - MarketingActivityExtensionSchema.parse({ - ...config, - fields: [ - { - ...config.fields[0], - ui_type: undefined, - }, - ], - }), - ).toThrowError( - new zod.ZodError([ - { - message: 'Field must have a ui_type', - code: zod.ZodIssueCode.custom, - path: ['fields', 0], - }, - ]), - ) - }) - - test('throws an error if ui_type is not supported', async () => { - // When/Then - expect(() => - MarketingActivityExtensionSchema.parse({ - ...config, - fields: [ - { - ...config.fields[0], - ui_type: 'not_a_ui_type', - }, - ], - }), - ).toThrowError( - new zod.ZodError([ - { - message: 'Unknown ui_type for Field: not_a_ui_type', - code: zod.ZodIssueCode.custom, - path: ['fields', 0], - }, - ]), - ) - }) - - test('throws an error if the schema for the ui_type is invalid', async () => { - // When/Then - expect(() => - MarketingActivityExtensionSchema.parse({ - ...config, - fields: [ - { - ...config.fields[0], - min_length: false, - }, - ], - }), - ).toThrowError( - new zod.ZodError([ - { - message: - 'Error found on Field "test_field": [\n {\n "code": "invalid_type",\n "expected": "number",\n "received": "boolean",\n "path": [\n "min_length"\n ],\n "message": "Expected number, received boolean"\n }\n]', - code: zod.ZodIssueCode.custom, - path: ['fields', 0], - }, - ]), - ) - }) - }) -}) diff --git a/packages/app/src/cli/models/extensions/specifications/marketing_activity_schemas/marketing_activity_schema.ts b/packages/app/src/cli/models/extensions/specifications/marketing_activity_schemas/marketing_activity_schema.ts deleted file mode 100644 index 338d911630b..00000000000 --- a/packages/app/src/cli/models/extensions/specifications/marketing_activity_schemas/marketing_activity_schema.ts +++ /dev/null @@ -1,186 +0,0 @@ -import {BaseSchema} from '../../schemas.js' -import {zod} from '@shopify/cli-kit/node/schema' - -const BaseFieldSchema = zod.object({ - ui_type: zod.string(), -}) - -const CommonFieldSchema = BaseFieldSchema.extend({ - name: zod.string(), - label: zod.string(), - help_text: zod.string().optional(), - required: zod.boolean(), -}) - -const BudgetScheduleFieldSchema = CommonFieldSchema.extend({ - ui_type: zod.literal('budget-schedule'), - use_scheduling: zod.boolean(), - use_end_date: zod.boolean(), - use_daily_budget: zod.boolean(), - use_lifetime_budget: zod.boolean(), -}) - -const DiscountPickerFieldSchema = CommonFieldSchema.extend({ - ui_type: zod.literal('discount-picker'), - min_resources: zod.number().nullable(), - max_resources: zod.number().nullable(), -}) - -const ScheduleFieldSchema = CommonFieldSchema.extend({ - ui_type: zod.literal('schedule'), - use_end_date: zod.boolean(), -}) - -const ProductPickerFieldSchema = CommonFieldSchema.extend({ - ui_type: zod.literal('product-picker'), - allow_product_image_selection: zod.boolean(), - allow_uploaded_image_as_product_image: zod.boolean(), - allow_free_image_as_product_image: zod.boolean(), - min_resources: zod.number().optional(), - max_resources: zod.number().optional(), - min_image_select_per_product: zod.number().optional(), - max_image_select_per_product: zod.number().optional(), -}) - -const SingleLineTextFieldSchema = CommonFieldSchema.extend({ - ui_type: zod.enum(['text-single-line', 'text-email', 'text-tel', 'text-url']), - placeholder: zod.string().optional(), - min_length: zod.number(), - max_length: zod.number(), -}) - -const TextMultiLineFieldSchema = CommonFieldSchema.extend({ - ui_type: zod.literal('text-multi-line'), - placeholder: zod.string(), - min_length: zod.number(), - max_length: zod.number(), -}) - -const DividerFieldSchema = BaseFieldSchema.extend({ - ui_type: zod.literal('divider'), - title: zod.string(), - name: zod.string(), -}) - -const SelectFieldSchema = CommonFieldSchema.extend({ - ui_type: zod.enum(['select-single', 'select-multiple']), - choices: zod.array( - zod.object({ - label: zod.string(), - value: zod.string(), - }), - ), -}) - -const ParagraphFieldSchema = BaseFieldSchema.extend({ - ui_type: zod.literal('paragraph'), - heading: zod.string().optional(), - body: zod.string().optional(), -}) - -const TypeAheadFieldSchema = CommonFieldSchema.extend({ - ui_type: zod.literal('type-ahead'), - placeholder: zod.string(), -}) - -const NumberFieldSchema = CommonFieldSchema.extend({ - ui_type: zod.enum(['number-float', 'number-integer']), - min: zod.number(), - max: zod.number(), - step: zod.number(), -}) - -const ImagePickerFieldSchema = CommonFieldSchema.extend({ - ui_type: zod.literal('image-picker'), - min_resources: zod.number(), - max_resources: zod.number(), - allow_free_images: zod.boolean(), - alt_text_required: zod.boolean(), -}) - -const UISchemaMapping: {[key: string]: zod.Schema} = { - 'budget-schedule': BudgetScheduleFieldSchema, - 'discount-picker': DiscountPickerFieldSchema, - schedule: ScheduleFieldSchema, - 'product-picker': ProductPickerFieldSchema, - 'text-single-line': SingleLineTextFieldSchema, - 'text-email': SingleLineTextFieldSchema, - 'text-tel': SingleLineTextFieldSchema, - 'text-url': SingleLineTextFieldSchema, - 'text-multi-line': TextMultiLineFieldSchema, - 'select-single': SelectFieldSchema, - 'select-multiple': SelectFieldSchema, - paragraph: ParagraphFieldSchema, - 'type-ahead': TypeAheadFieldSchema, - 'number-float': NumberFieldSchema, - 'number-integer': NumberFieldSchema, - 'image-picker': ImagePickerFieldSchema, - divider: DividerFieldSchema, -} - -export const MarketingActivityExtensionSchema = BaseSchema.extend({ - title: zod.string().min(1), - description: zod.string().min(1), - api_path: zod.string(), - tactic: zod.enum([ - 'ad', - 'retargeting', - 'post', - 'message', - 'transactional', - 'newsletter', - 'abandoned_cart', - 'affililate', - 'loyalty', - 'link', - 'storefront_app', - ]), - marketing_channel: zod.enum(['social', 'search', 'email', 'sms', 'display', 'marketplace']), - referring_domain: zod.string().optional(), - is_automation: zod.boolean().optional(), - use_external_editor: zod.boolean().optional(), - preview_data: zod.object({ - types: zod - .array( - zod.object({ - label: zod.string(), - value: zod.string(), - }), - ) - .max(3) - .min(1), - }), - fields: zod - .array( - zod.any().superRefine((val, ctx) => { - if (typeof val !== 'object') { - return ctx.addIssue({ - message: 'Field must be an object', - code: zod.ZodIssueCode.custom, - }) - } - if (val.ui_type === undefined) { - return ctx.addIssue({ - message: 'Field must have a ui_type', - code: zod.ZodIssueCode.custom, - }) - } - const schema = UISchemaMapping[val.ui_type] - if (schema === undefined) { - return ctx.addIssue({ - message: `Unknown ui_type for Field: ${val.ui_type}`, - code: zod.ZodIssueCode.custom, - }) - } - - const result = schema.safeParse(val) - if (!result.success) { - return ctx.addIssue({ - message: `Error found on Field "${val.name}": ${result.error.message}`, - code: zod.ZodIssueCode.custom, - }) - } - }), - ) - .min(1), -}) diff --git a/packages/app/src/cli/services/app/toml-patch-wasm.test.ts b/packages/app/src/cli/services/app/toml-patch-wasm.test.ts deleted file mode 100644 index 75056abfd10..00000000000 --- a/packages/app/src/cli/services/app/toml-patch-wasm.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import {updateTomlValues} from './toml-patch-wasm.js' -import {describe, expect, test} from 'vitest' - -// Sample TOML content for testing -const sampleToml = ` -# This is a sample TOML file -title = "TOML Example" - -[owner] -name = "Test User" -organization = "Test Org" - -[database] -server = "192.168.1.1" -ports = [ 8001, 8001, 8002 ] # Comment after array -enabled = true -` - -describe('WASM TOML Patch Integration', () => { - test('updating TOML makes minimal changes and preserves as much as possible', async () => { - const output = await updateTomlValues(sampleToml, [ - [['owner', 'dotted', 'notation'], 123.5], - [['database', 'server'], 'changed'], - [['top_level'], true], - [['owner', 'organization'], undefined], - [ - ['database', 'backup_ports'], - [8003, 8004], - ], - ]) - - const expected = ` -# This is a sample TOML file -title = "TOML Example" -top_level = true - -[owner] -name = "Test User" -dotted.notation = 123.5 - -[database] -server = "changed" -ports = [ 8001, 8001, 8002 ] # Comment after array -enabled = true -backup_ports = [8003, 8004] -` - - expect(output).toBe(expected) - }) -}) diff --git a/packages/app/src/cli/services/app/toml-patch-wasm.ts b/packages/app/src/cli/services/app/toml-patch-wasm.ts deleted file mode 100644 index 001e8c338ad..00000000000 --- a/packages/app/src/cli/services/app/toml-patch-wasm.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as tomlPatch from '@shopify/toml-patch' - -type TomlSingleValue = string | number | boolean - -type TomlPatchValue = TomlSingleValue | TomlSingleValue[] | undefined - -/** - * Update the TOML content using the WASM module - * @param tomlContent - The TOML content to update - * @param patches - An array of tuples, each containing a dotted path and a value - * @returns The updated TOML content - */ -export async function updateTomlValues(tomlContent: string, patches: [string[], TomlPatchValue][]): Promise { - return tomlPatch.updateTomlValues(tomlContent, patches) -} diff --git a/packages/cli/src/cli/services/upgrade.test.ts b/packages/cli/src/cli/services/upgrade.test.ts deleted file mode 100644 index 08407731792..00000000000 --- a/packages/cli/src/cli/services/upgrade.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import {upgrade} from './upgrade.js' -import * as upgradeService from './upgrade.js' -import {afterEach, beforeEach, describe, expect, vi, test} from 'vitest' -import {platformAndArch} from '@shopify/cli-kit/node/os' -import * as nodePackageManager from '@shopify/cli-kit/node/node-package-manager' -import {exec, captureOutput} from '@shopify/cli-kit/node/system' -import {inTemporaryDirectory, touchFile, writeFile} from '@shopify/cli-kit/node/fs' -import {joinPath, normalizePath} from '@shopify/cli-kit/node/path' -import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output' -import {AbortError} from '@shopify/cli-kit/node/error' - -const oldCliVersion = '3.0.0' -// just needs to be higher than oldCliVersion for these tests -const currentCliVersion = '3.10.0' - -vi.mock('@shopify/cli-kit/node/os', async () => { - return { - platformAndArch: vi.fn(), - } -}) -vi.mock('@shopify/cli-kit/node/system') - -beforeEach(async () => { - vi.mocked(platformAndArch).mockReturnValue({platform: 'windows', arch: 'amd64'}) -}) -afterEach(() => { - mockAndCaptureOutput().clear() -}) - -describe('upgrade global CLI', () => { - test('does not upgrade globally if the latest version is found', async () => { - await inTemporaryDirectory(async (tmpDir) => { - // Given - const outputMock = mockAndCaptureOutput() - vi.spyOn(nodePackageManager as any, 'checkForNewVersion').mockResolvedValue(undefined) - - // When - await upgrade(tmpDir, currentCliVersion, {env: {}}) - - // Then - expect(outputMock.info()).toMatchInlineSnapshot(` - "You're on the latest version, ${currentCliVersion}, no need to upgrade!" - `) - }) - }) - - test('upgrades globally using npm if the latest version is not found', async () => { - await inTemporaryDirectory(async (tmpDir) => { - // Given - const outputMock = mockAndCaptureOutput() - vi.spyOn(nodePackageManager as any, 'checkForNewVersion').mockResolvedValue(currentCliVersion) - - // When - await upgrade(tmpDir, oldCliVersion, {env: {}}) - - // Then - expect(vi.mocked(exec)).toHaveBeenCalledWith( - 'npm', - ['install', '-g', '@shopify/cli@latest', '@shopify/theme@latest'], - {stdio: 'inherit'}, - ) - expect(outputMock.info()).toMatchInlineSnapshot(` - "Upgrading CLI from ${oldCliVersion} to ${currentCliVersion}...\nAttempting to upgrade via \`npm install -g @shopify/cli@latest @shopify/theme@latest\`..." - `) - expect(outputMock.success()).toMatchInlineSnapshot(` - "Upgraded Shopify CLI to version ${currentCliVersion}" - `) - }) - }) - - const homebrewPackageNames = ['shopify-cli', 'shopify-cli@3'] - homebrewPackageNames.forEach((homebrewPackageName: string) => { - test('upgrades globally using Homebrew if the latest version is not found and the CLI was installed via Homebrew', async () => { - await inTemporaryDirectory(async (tmpDir) => { - // Given - vi.spyOn(nodePackageManager as any, 'checkForNewVersion').mockResolvedValue(currentCliVersion) - - // Then - await expect(async () => { - await upgrade(tmpDir, oldCliVersion, {env: {SHOPIFY_HOMEBREW_FORMULA: homebrewPackageName}}) - }).rejects.toThrowError(AbortError) - }) - }) - }) -}) - -describe('upgrade local CLI', () => { - test('throws an error if a valid app config file is missing', async () => { - await inTemporaryDirectory(async (tmpDir) => { - // Given - await Promise.all([ - writeFile( - joinPath(tmpDir, 'package.json'), - JSON.stringify({dependencies: {'@shopify/cli': currentCliVersion, '@shopify/app': currentCliVersion}}), - ), - touchFile(joinPath(tmpDir, 'shopify.wrongapp.toml')), - ]) - const outputMock = mockAndCaptureOutput() - vi.spyOn(nodePackageManager as any, 'checkForNewVersion').mockResolvedValue(undefined) - - // When // Then - await expect(upgrade(tmpDir, currentCliVersion, {env: {npm_config_user_agent: 'npm'}})).rejects.toBeInstanceOf( - AbortError, - ) - }) - }) - - test('does not upgrade locally if the latest version is found', async () => { - await inTemporaryDirectory(async (tmpDir) => { - // Given - await Promise.all([ - writeFile( - joinPath(tmpDir, 'package.json'), - JSON.stringify({dependencies: {'@shopify/cli': currentCliVersion, '@shopify/app': currentCliVersion}}), - ), - touchFile(joinPath(tmpDir, 'shopify.app.toml')), - ]) - const outputMock = mockAndCaptureOutput() - vi.spyOn(nodePackageManager as any, 'checkForNewVersion').mockResolvedValue(undefined) - - // When - await upgrade(tmpDir, currentCliVersion, {env: {npm_config_user_agent: 'npm'}}) - - // Then - expect(outputMock.info()).toMatchInlineSnapshot(` - "You're on the latest version, ${currentCliVersion}, no need to upgrade!" - `) - }) - }) - - test('upgrades locally if the latest version is not found', async () => { - await inTemporaryDirectory(async (tmpDir) => { - // Given - await Promise.all([ - writeFile( - joinPath(tmpDir, 'package.json'), - JSON.stringify({dependencies: {'@shopify/cli': oldCliVersion, '@shopify/app': oldCliVersion}}), - ), - touchFile(joinPath(tmpDir, 'shopify.app.toml')), - ]) - vi.mocked(captureOutput).mockResolvedValueOnce(tmpDir) - - const outputMock = mockAndCaptureOutput() - vi.spyOn(nodePackageManager as any, 'checkForNewVersion').mockResolvedValueOnce(currentCliVersion) - const addNPMDependenciesMock = vi - .spyOn(nodePackageManager as any, 'addNPMDependencies') - .mockResolvedValue(undefined) - - // When - await upgradeService.upgrade(tmpDir, oldCliVersion, {env: {}}) - - // Then - expect(captureOutput).toHaveBeenCalledWith('npm', ['prefix'], {cwd: normalizePath(tmpDir)}) - expect(outputMock.info()).toMatchInlineSnapshot(` - "Upgrading CLI from ${oldCliVersion} to ${currentCliVersion}..." - `) - expect(addNPMDependenciesMock).toHaveBeenCalledWith([{name: '@shopify/cli', version: 'latest'}], { - packageManager: 'npm', - type: 'prod', - directory: normalizePath(tmpDir), - stdout: process.stdout, - stderr: process.stderr, - addToRootDirectory: false, - }) - expect(outputMock.success()).toMatchInlineSnapshot(` - "Upgraded Shopify CLI to version ${currentCliVersion}" - `) - }) - }) - - test('upgrades locally if CLI is on latest version but APP isnt', async () => { - await inTemporaryDirectory(async (tmpDir) => { - // Given - await Promise.all([ - writeFile( - joinPath(tmpDir, 'package.json'), - JSON.stringify({dependencies: {'@shopify/cli': currentCliVersion, '@shopify/app': oldCliVersion}}), - ), - touchFile(joinPath(tmpDir, 'shopify.app.nondefault.toml')), - ]) - vi.mocked(captureOutput).mockResolvedValueOnce(tmpDir) - - const outputMock = mockAndCaptureOutput() - const checkMock = vi.spyOn(nodePackageManager as any, 'checkForNewVersion') - checkMock.mockResolvedValueOnce(undefined).mockResolvedValueOnce(currentCliVersion) - const addNPMDependenciesMock = vi - .spyOn(nodePackageManager as any, 'addNPMDependencies') - .mockResolvedValue(undefined) - - // When - await upgradeService.upgrade(tmpDir, oldCliVersion, {env: {}}) - - // Then - expect(captureOutput).toHaveBeenCalledWith('npm', ['prefix'], {cwd: normalizePath(tmpDir)}) - expect(outputMock.info()).toMatchInlineSnapshot(` - "Upgrading CLI from ${oldCliVersion} to ${currentCliVersion}..." - `) - expect(addNPMDependenciesMock).toHaveBeenCalledWith([{name: '@shopify/cli', version: 'latest'}], { - packageManager: 'npm', - type: 'prod', - directory: normalizePath(tmpDir), - stdout: process.stdout, - stderr: process.stderr, - addToRootDirectory: false, - }) - expect(outputMock.success()).toMatchInlineSnapshot(` - "Upgraded Shopify CLI to version ${currentCliVersion}" - `) - }) - }) -}) diff --git a/packages/cli/src/cli/services/upgrade.ts b/packages/cli/src/cli/services/upgrade.ts deleted file mode 100644 index 0ae3524bd3d..00000000000 --- a/packages/cli/src/cli/services/upgrade.ts +++ /dev/null @@ -1,196 +0,0 @@ -/* eslint-disable @typescript-eslint/no-invalid-void-type */ -import { - addNPMDependencies, - findUpAndReadPackageJson, - checkForNewVersion, - DependencyType, - getPackageManager, - PackageJson, - usesWorkspaces, -} from '@shopify/cli-kit/node/node-package-manager' -import {exec} from '@shopify/cli-kit/node/system' -import {dirname, joinPath, moduleDirectory} from '@shopify/cli-kit/node/path' -import {findPathUp, glob} from '@shopify/cli-kit/node/fs' -import {AbortError} from '@shopify/cli-kit/node/error' -import {outputContent, outputInfo, outputSuccess, outputToken, outputWarn} from '@shopify/cli-kit/node/output' - -type HomebrewPackageName = 'shopify-cli' | 'shopify-cli@3' - -// Canonical list of oclif plugins that should be installed globally -const globalPlugins = ['@shopify/theme'] - -interface UpgradeOptions { - env: NodeJS.ProcessEnv -} - -export async function upgrade( - directory: string, - currentVersion: string, - {env}: UpgradeOptions = {env: process.env}, -): Promise { - let newestVersion: string | void - - const projectDir = await getProjectDir(directory) - if (projectDir) { - newestVersion = await upgradeLocalShopify(projectDir, currentVersion) - } else if (usingPackageManager({env})) { - throw new AbortError( - outputContent`Couldn't find an app toml file at ${outputToken.path( - directory, - )}, is this a Shopify project directory?`, - ) - } else { - newestVersion = await upgradeGlobalShopify(currentVersion, {env}) - } - - if (newestVersion) { - outputSuccess(`Upgraded Shopify CLI to version ${newestVersion}`) - } -} - -async function getProjectDir(directory: string): Promise { - const configFiles = ['shopify.app{,.*}.toml', 'hydrogen.config.js', 'hydrogen.config.ts'] - const existsConfigFile = async (directory: string) => { - const configPaths = await glob(configFiles.map((file) => joinPath(directory, file))) - return configPaths.length > 0 ? configPaths[0] : undefined - } - const configFile = await findPathUp(existsConfigFile, { - cwd: directory, - type: 'file', - }) - if (configFile) return dirname(configFile) -} - -async function upgradeLocalShopify(projectDir: string, currentVersion: string): Promise { - const packageJson = (await findUpAndReadPackageJson(projectDir)).content - const packageJsonDependencies = packageJson.dependencies || {} - const packageJsonDevDependencies = packageJson.devDependencies || {} - const allDependencies = {...packageJsonDependencies, ...packageJsonDevDependencies} - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - let resolvedCLIVersion = allDependencies[await cliDependency()]! - const resolvedAppVersion = allDependencies['@shopify/app']?.replace(/[\^~]/, '') - - if (resolvedCLIVersion.slice(0, 1).match(/[\^~]/)) resolvedCLIVersion = currentVersion - const newestCLIVersion = await checkForNewVersion(await cliDependency(), resolvedCLIVersion) - const newestAppVersion = resolvedAppVersion ? await checkForNewVersion('@shopify/app', resolvedAppVersion) : undefined - - if (newestCLIVersion) { - outputUpgradeMessage(resolvedCLIVersion, newestCLIVersion) - } else if (resolvedAppVersion && newestAppVersion) { - outputUpgradeMessage(resolvedAppVersion, newestAppVersion) - } else { - outputWontInstallMessage(resolvedCLIVersion) - return - } - - await installJsonDependencies('prod', packageJsonDependencies, projectDir) - await installJsonDependencies('dev', packageJsonDevDependencies, projectDir) - return newestCLIVersion ?? newestAppVersion -} - -async function upgradeGlobalShopify( - currentVersion: string, - {env}: UpgradeOptions = {env: process.env}, -): Promise { - const newestVersion = await checkForNewVersion(await cliDependency(), currentVersion) - - if (!newestVersion) { - outputWontInstallMessage(currentVersion) - return - } - - outputUpgradeMessage(currentVersion, newestVersion) - - const homebrewPackage = env.SHOPIFY_HOMEBREW_FORMULA as HomebrewPackageName | undefined - try { - if (homebrewPackage) { - throw new AbortError( - outputContent`Upgrade only works for packages managed by a Node package manager (e.g. npm). Run ${outputToken.genericShellCommand( - 'brew upgrade && brew update', - )} instead`, - ) - } else { - await upgradeGlobalViaNpm() - } - } catch (err) { - outputWarn('Upgrade failed!') - throw err - } - return newestVersion -} - -async function upgradeGlobalViaNpm(): Promise { - const command = 'npm' - const args = [ - 'install', - '-g', - `${await cliDependency()}@latest`, - ...globalPlugins.map((plugin) => `${plugin}@latest`), - ] - outputInfo( - outputContent`Attempting to upgrade via ${outputToken.genericShellCommand([command, ...args].join(' '))}...`, - ) - await exec(command, args, {stdio: 'inherit'}) -} - -function outputWontInstallMessage(currentVersion: string): void { - outputInfo(outputContent`You're on the latest version, ${outputToken.yellow(currentVersion)}, no need to upgrade!`) -} - -function outputUpgradeMessage(currentVersion: string, newestVersion: string): void { - outputInfo( - outputContent`Upgrading CLI from ${outputToken.yellow(currentVersion)} to ${outputToken.yellow(newestVersion)}...`, - ) -} - -async function installJsonDependencies( - depsEnv: DependencyType, - deps: {[key: string]: string}, - directory: string, -): Promise { - const packagesToUpdate = [await cliDependency(), ...(await oclifPlugins())] - .filter((pkg: string): boolean => { - const pkgRequirement: string | undefined = deps[pkg] - return Boolean(pkgRequirement) - }) - .map((pkg) => { - return {name: pkg, version: 'latest'} - }) - - const appUsesWorkspaces = await usesWorkspaces(directory) - - if (packagesToUpdate.length > 0) { - await addNPMDependencies(packagesToUpdate, { - packageManager: await getPackageManager(directory), - type: depsEnv, - directory, - stdout: process.stdout, - stderr: process.stderr, - addToRootDirectory: appUsesWorkspaces, - }) - } -} - -async function cliDependency(): Promise { - return (await packageJsonContents()).name -} - -async function oclifPlugins(): Promise { - return (await packageJsonContents())?.oclif?.plugins || [] -} - -type PackageJsonWithName = Omit & {name: string} -let _packageJsonContents: PackageJsonWithName | undefined - -async function packageJsonContents(): Promise { - if (!_packageJsonContents) { - const packageJson = await findUpAndReadPackageJson(moduleDirectory(import.meta.url)) - _packageJsonContents = _packageJsonContents || (packageJson.content as PackageJsonWithName) - } - return _packageJsonContents -} - -function usingPackageManager({env}: UpgradeOptions = {env: process.env}): boolean { - return Boolean(env.npm_config_user_agent) -}