From 6320872afd0fc918f34a1970236e89b4ce3f8308 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 3 Jun 2025 13:18:08 -0600 Subject: [PATCH 1/3] feat: only warn about old version mismatches --- package.json | 1 + src/hooks/prerun.ts | 30 ++++++-- test/hooks/prerun.test.ts | 152 ++++++++++++++++++++++++++++++++++++++ yarn.lock | 8 ++ 4 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 test/hooks/prerun.test.ts diff --git a/package.json b/package.json index e042710ba..b8fe0bd7d 100644 --- a/package.json +++ b/package.json @@ -257,6 +257,7 @@ "types": "dist/index.d.ts", "devDependencies": { "@oclif/plugin-command-snapshot": "^5.2.3", + "@oclif/test": "^4.1.13", "@salesforce/dev-scripts": "^11.0.2", "@salesforce/plugin-release-management": "^5.7.0", "@salesforce/ts-sinon": "^1.4.30", diff --git a/src/hooks/prerun.ts b/src/hooks/prerun.ts index dd1ad308c..beb4fa8cf 100644 --- a/src/hooks/prerun.ts +++ b/src/hooks/prerun.ts @@ -7,9 +7,8 @@ import { type Hook } from '@oclif/core/hooks'; -// eslint-disable-next-line @typescript-eslint/require-await -const hook: Hook.Prerun = async function ({ Command, config }) { - if (process.argv.includes('--json')) return; +const hook: Hook.Prerun = async function ({ Command, config, argv }) { + if (argv.includes('--json') || process.argv.includes('--json')) return; const { plugin } = Command; if (!plugin) return; if (plugin.type === 'link') return; @@ -20,10 +19,31 @@ const hook: Hook.Prerun = async function ({ Command, config }) { const specifiedVersion = jitPlugins[plugin.name] ?? deps[plugin.name]; if (!specifiedVersion) return; - if (plugin.version !== specifiedVersion) { + // Simple semver comparison without external library + const parseVersion = (version: string): number[] => + version + .replace(/^[^\d]*/, '') + .split('.') + .map((n) => parseInt(n, 10) || 0); + + const isLessThan = (version1: string, version2: string): boolean => { + const v1 = parseVersion(version1); + const v2 = parseVersion(version2); + + for (let i = 0; i < Math.max(v1.length, v2.length); i++) { + const num1 = v1[i] || 0; + const num2 = v2[i] || 0; + + if (num1 < num2) return true; + if (num1 > num2) return false; + } + return false; + }; + + if (isLessThan(plugin.version, specifiedVersion)) { const { ux } = await import('@oclif/core/ux'); ux.warn( - `Plugin ${plugin.name} (${plugin.version}) differs from the version specified by ${config.bin} (${specifiedVersion})` + `Plugin ${plugin.name} (${plugin.version}) is older than the version specified by ${config.bin} (${specifiedVersion})` ); } }; diff --git a/test/hooks/prerun.test.ts b/test/hooks/prerun.test.ts new file mode 100644 index 000000000..3ac8c59a3 --- /dev/null +++ b/test/hooks/prerun.test.ts @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { expect } from 'chai'; +import { runHook } from '@oclif/test'; +import { Config } from '@oclif/core/config'; + +describe('Prerun Hook', () => { + const WARNING_TEXT = 'is older than the version specified by'; + it('should do nothing when --json is present', async () => { + const { stderr } = await runHook('prerun', { argv: ['--json'] }); + expect(stderr).to.not.include(WARNING_TEXT); + }); + + it('should do nothing if plugin type is "link"', async () => { + const command = { + type: 'link', + }; + const { stderr } = await runHook('prerun', { Command: command, argv: [] }); + expect(stderr).to.not.include(WARNING_TEXT); + }); + + it('should do nothing if plugin is not JIT or core', async () => { + const command = { + plugin: { + type: 'core', + name: '@salesforce/plugin-foo', + version: '1.0.0', + }, + }; + const { stderr } = await runHook('prerun', { Command: command, argv: [] }); + expect(stderr).to.not.include(WARNING_TEXT); + }); + + it('should warn if plugin version is less than specified version', async () => { + const command = { + plugin: { + type: 'jit', + name: '@salesforce/plugin-packaging', + version: '0.0.1', + }, + }; + const { stderr } = await runHook('prerun', { Command: command, argv: [] }); + expect(stderr).to.include(WARNING_TEXT); + }); + + it('should not warn if plugin version is greater than specified version', async () => { + const command = { + plugin: { + type: 'jit', + name: '@salesforce/plugin-packaging', + version: '10000.0.0', + }, + }; + const { stderr } = await runHook('prerun', { Command: command, argv: [] }); + expect(stderr).to.not.include(WARNING_TEXT); + }); + + it('should not warn if plugin version is equal to specified version', async () => { + const command = { + plugin: { + type: 'jit', + name: '@salesforce/plugin-packaging', + version: '1.0.0', + }, + }; + const config = await Config.load({ root: dirname(fileURLToPath(import.meta.url)) }); + + config.pjson.dependencies = { + ...config.pjson.dependencies, + '@salesforce/plugin-packaging': '1.0.0', + }; + const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config); + expect(stderr).to.not.include(WARNING_TEXT); + }); + + it('should warn if plugin version is an older prerelease', async () => { + const command = { + plugin: { + type: 'jit', + name: '@salesforce/plugin-packaging', + version: '1.0.0-beta.1', + }, + }; + const config = await Config.load({ root: dirname(fileURLToPath(import.meta.url)) }); + + config.pjson.dependencies = { + ...config.pjson.dependencies, + '@salesforce/plugin-packaging': '1.0.0-beta.2', + }; + const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config); + expect(stderr).to.include(WARNING_TEXT); + }); + + it('should not warn if plugin version is a newer prerelease', async () => { + const command = { + plugin: { + type: 'jit', + name: '@salesforce/plugin-packaging', + version: '1.0.0-beta.2', + }, + }; + const config = await Config.load({ root: dirname(fileURLToPath(import.meta.url)) }); + + config.pjson.dependencies = { + ...config.pjson.dependencies, + '@salesforce/plugin-packaging': '1.0.0-beta.1', + }; + const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config); + expect(stderr).to.not.include(WARNING_TEXT); + }); + + it('should not warn if plugin version is a newer prerelease and specified version is an older stable release', async () => { + const command = { + plugin: { + type: 'jit', + name: '@salesforce/plugin-packaging', + version: '1.0.0-beta.1', + }, + }; + const config = await Config.load({ root: dirname(fileURLToPath(import.meta.url)) }); + + config.pjson.dependencies = { + ...config.pjson.dependencies, + '@salesforce/plugin-packaging': '1.0.0', + }; + const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config); + expect(stderr).to.not.include(WARNING_TEXT); + }); + + it('should warn if plugin version is an older prerelease and specified version is a newer stable release', async () => { + const command = { + plugin: { + type: 'jit', + name: '@salesforce/plugin-packaging', + version: '1.0.0-beta.1', + }, + }; + const config = await Config.load({ root: dirname(fileURLToPath(import.meta.url)) }); + config.pjson.dependencies = { + ...config.pjson.dependencies, + '@salesforce/plugin-packaging': '1.0.1', + }; + const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config); + expect(stderr).to.include(WARNING_TEXT); + }); +}); diff --git a/yarn.lock b/yarn.lock index 56fee0efc..8421702b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1785,6 +1785,14 @@ strip-ansi "^7.1.0" wrap-ansi "^9.0.0" +"@oclif/test@^4.1.13": + version "4.1.13" + resolved "https://registry.yarnpkg.com/@oclif/test/-/test-4.1.13.tgz#b39b541f2edf45828aafb8d7df635516e0356b83" + integrity sha512-pulrTiJRhoAKizFf6y5WeHvM2JyoRiZKV0H8qqYEoE0UHDKqInNmfGJyp8Ip6lTVQeMv1U8YCAXOS/HiWPVWeg== + dependencies: + ansis "^3.17.0" + debug "^4.4.1" + "@octokit/auth-token@^5.0.0": version "5.1.2" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-5.1.2.tgz#68a486714d7a7fd1df56cb9bc89a860a0de866de" From 8820139b1c4d96651bfdc6eb3c0dee8526b6e792 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 3 Jun 2025 13:37:37 -0600 Subject: [PATCH 2/3] test: use regex --- test/hooks/prerun.test.ts | 56 +++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/test/hooks/prerun.test.ts b/test/hooks/prerun.test.ts index 3ac8c59a3..f81f39871 100644 --- a/test/hooks/prerun.test.ts +++ b/test/hooks/prerun.test.ts @@ -10,11 +10,26 @@ import { expect } from 'chai'; import { runHook } from '@oclif/test'; import { Config } from '@oclif/core/config'; +function sanitize(stderr: string): string { + return stderr + .replaceAll(' › ', ' ') + .split('\n') + .map((s) => s.trim()) + .join(' '); +} + describe('Prerun Hook', () => { - const WARNING_TEXT = 'is older than the version specified by'; + const WARNING_REGEX = + /Warning: Plugin ([^ ]+) \((\d+\.\d+\.\d+(?:-[^)]*)?)\) is older than the version specified by ([^ ]+) \((\d+\.\d+\.\d+(?:-[^)]*)?)\)/; + let config: Config; + + before(async () => { + config = await Config.load({ root: dirname(fileURLToPath(import.meta.url)) }); + }); + it('should do nothing when --json is present', async () => { const { stderr } = await runHook('prerun', { argv: ['--json'] }); - expect(stderr).to.not.include(WARNING_TEXT); + expect(sanitize(stderr)).to.not.match(WARNING_REGEX); }); it('should do nothing if plugin type is "link"', async () => { @@ -22,7 +37,7 @@ describe('Prerun Hook', () => { type: 'link', }; const { stderr } = await runHook('prerun', { Command: command, argv: [] }); - expect(stderr).to.not.include(WARNING_TEXT); + expect(sanitize(stderr)).to.not.match(WARNING_REGEX); }); it('should do nothing if plugin is not JIT or core', async () => { @@ -34,119 +49,114 @@ describe('Prerun Hook', () => { }, }; const { stderr } = await runHook('prerun', { Command: command, argv: [] }); - expect(stderr).to.not.include(WARNING_TEXT); + expect(sanitize(stderr)).to.not.match(WARNING_REGEX); }); it('should warn if plugin version is less than specified version', async () => { const command = { plugin: { - type: 'jit', + type: 'core', name: '@salesforce/plugin-packaging', version: '0.0.1', }, }; const { stderr } = await runHook('prerun', { Command: command, argv: [] }); - expect(stderr).to.include(WARNING_TEXT); + expect(sanitize(stderr)).to.match(WARNING_REGEX); }); it('should not warn if plugin version is greater than specified version', async () => { const command = { plugin: { - type: 'jit', + type: 'core', name: '@salesforce/plugin-packaging', version: '10000.0.0', }, }; const { stderr } = await runHook('prerun', { Command: command, argv: [] }); - expect(stderr).to.not.include(WARNING_TEXT); + expect(sanitize(stderr)).to.not.match(WARNING_REGEX); }); it('should not warn if plugin version is equal to specified version', async () => { const command = { plugin: { - type: 'jit', + type: 'core', name: '@salesforce/plugin-packaging', version: '1.0.0', }, }; - const config = await Config.load({ root: dirname(fileURLToPath(import.meta.url)) }); config.pjson.dependencies = { ...config.pjson.dependencies, '@salesforce/plugin-packaging': '1.0.0', }; const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config); - expect(stderr).to.not.include(WARNING_TEXT); + expect(sanitize(stderr)).to.not.match(WARNING_REGEX); }); it('should warn if plugin version is an older prerelease', async () => { const command = { plugin: { - type: 'jit', + type: 'core', name: '@salesforce/plugin-packaging', version: '1.0.0-beta.1', }, }; - const config = await Config.load({ root: dirname(fileURLToPath(import.meta.url)) }); config.pjson.dependencies = { ...config.pjson.dependencies, '@salesforce/plugin-packaging': '1.0.0-beta.2', }; const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config); - expect(stderr).to.include(WARNING_TEXT); + expect(sanitize(stderr)).to.match(WARNING_REGEX); }); it('should not warn if plugin version is a newer prerelease', async () => { const command = { plugin: { - type: 'jit', + type: 'core', name: '@salesforce/plugin-packaging', version: '1.0.0-beta.2', }, }; - const config = await Config.load({ root: dirname(fileURLToPath(import.meta.url)) }); config.pjson.dependencies = { ...config.pjson.dependencies, '@salesforce/plugin-packaging': '1.0.0-beta.1', }; const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config); - expect(stderr).to.not.include(WARNING_TEXT); + expect(sanitize(stderr)).to.not.match(WARNING_REGEX); }); it('should not warn if plugin version is a newer prerelease and specified version is an older stable release', async () => { const command = { plugin: { - type: 'jit', + type: 'core', name: '@salesforce/plugin-packaging', version: '1.0.0-beta.1', }, }; - const config = await Config.load({ root: dirname(fileURLToPath(import.meta.url)) }); config.pjson.dependencies = { ...config.pjson.dependencies, '@salesforce/plugin-packaging': '1.0.0', }; const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config); - expect(stderr).to.not.include(WARNING_TEXT); + expect(sanitize(stderr)).to.not.match(WARNING_REGEX); }); it('should warn if plugin version is an older prerelease and specified version is a newer stable release', async () => { const command = { plugin: { - type: 'jit', + type: 'core', name: '@salesforce/plugin-packaging', version: '1.0.0-beta.1', }, }; - const config = await Config.load({ root: dirname(fileURLToPath(import.meta.url)) }); config.pjson.dependencies = { ...config.pjson.dependencies, '@salesforce/plugin-packaging': '1.0.1', }; const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config); - expect(stderr).to.include(WARNING_TEXT); + expect(sanitize(stderr)).to.match(WARNING_REGEX); }); }); From bfa5ff2b550d136d23cb75ce058939e3aed03142 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 3 Jun 2025 13:54:00 -0600 Subject: [PATCH 3/3] test: handle windows --- test/hooks/prerun.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/hooks/prerun.test.ts b/test/hooks/prerun.test.ts index f81f39871..7150d9f19 100644 --- a/test/hooks/prerun.test.ts +++ b/test/hooks/prerun.test.ts @@ -12,7 +12,8 @@ import { Config } from '@oclif/core/config'; function sanitize(stderr: string): string { return stderr - .replaceAll(' › ', ' ') + .replaceAll('› ', ' ') + .replaceAll('» ', ' ') .split('\n') .map((s) => s.trim()) .join(' ');