Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 25 additions & 5 deletions src/hooks/prerun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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})`
);
}
};
Expand Down
163 changes: 163 additions & 0 deletions test/hooks/prerun.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* 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';

function sanitize(stderr: string): string {
return stderr
.replaceAll('› ', ' ')
.replaceAll('» ', ' ')
.split('\n')
.map((s) => s.trim())
.join(' ');
}

describe('Prerun Hook', () => {
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(sanitize(stderr)).to.not.match(WARNING_REGEX);
});

it('should do nothing if plugin type is "link"', async () => {
const command = {
type: 'link',
};
const { stderr } = await runHook('prerun', { Command: command, argv: [] });
expect(sanitize(stderr)).to.not.match(WARNING_REGEX);
});

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(sanitize(stderr)).to.not.match(WARNING_REGEX);
});

it('should warn if plugin version is less than specified version', async () => {
const command = {
plugin: {
type: 'core',
name: '@salesforce/plugin-packaging',
version: '0.0.1',
},
};
const { stderr } = await runHook('prerun', { Command: command, argv: [] });
expect(sanitize(stderr)).to.match(WARNING_REGEX);
});

it('should not warn if plugin version is greater than specified version', async () => {
const command = {
plugin: {
type: 'core',
name: '@salesforce/plugin-packaging',
version: '10000.0.0',
},
};
const { stderr } = await runHook('prerun', { Command: command, argv: [] });
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: 'core',
name: '@salesforce/plugin-packaging',
version: '1.0.0',
},
};

config.pjson.dependencies = {
...config.pjson.dependencies,
'@salesforce/plugin-packaging': '1.0.0',
};
const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config);
expect(sanitize(stderr)).to.not.match(WARNING_REGEX);
});

it('should warn if plugin version is an older prerelease', async () => {
const command = {
plugin: {
type: 'core',
name: '@salesforce/plugin-packaging',
version: '1.0.0-beta.1',
},
};

config.pjson.dependencies = {
...config.pjson.dependencies,
'@salesforce/plugin-packaging': '1.0.0-beta.2',
};
const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config);
expect(sanitize(stderr)).to.match(WARNING_REGEX);
});

it('should not warn if plugin version is a newer prerelease', async () => {
const command = {
plugin: {
type: 'core',
name: '@salesforce/plugin-packaging',
version: '1.0.0-beta.2',
},
};

config.pjson.dependencies = {
...config.pjson.dependencies,
'@salesforce/plugin-packaging': '1.0.0-beta.1',
};
const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config);
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: 'core',
name: '@salesforce/plugin-packaging',
version: '1.0.0-beta.1',
},
};

config.pjson.dependencies = {
...config.pjson.dependencies,
'@salesforce/plugin-packaging': '1.0.0',
};
const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config);
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: 'core',
name: '@salesforce/plugin-packaging',
version: '1.0.0-beta.1',
},
};
config.pjson.dependencies = {
...config.pjson.dependencies,
'@salesforce/plugin-packaging': '1.0.1',
};
const { stderr } = await runHook('prerun', { Command: command, argv: [] }, config);
expect(sanitize(stderr)).to.match(WARNING_REGEX);
});
});
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading