Skip to content
Open
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
20 changes: 10 additions & 10 deletions src/m365/spfx/commands/project/project-upgrade.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1552,47 +1552,47 @@ describe(commands.PROJECT_UPGRADE, () => {

await command.action(logger, { options: { toVersion: '1.5.0', output: 'json' } } as any);
const findings: FindingToReport[] = log[0];
assert.strictEqual(findings.length, 26);
assert.strictEqual(findings.length, 25);
});

it('e2e: shows correct number of findings for upgrading react web part 1.4.1 project to 1.5.0', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-141-webpart-react'));

await command.action(logger, { options: { toVersion: '1.5.0', debug: true, output: 'json' } } as any);
const findings: Finding[] = log[3];
assert.strictEqual(findings.length, 26);
assert.strictEqual(findings.length, 25);
});

it('e2e: shows correct number of findings for upgrading web part with optional dependencies 1.4.1 project to 1.5.0', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-141-webpart-optionaldeps'));

await command.action(logger, { options: { toVersion: '1.5.0', output: 'json' } } as any);
const findings: FindingToReport[] = log[0];
assert.strictEqual(findings.length, 33);
assert.strictEqual(findings.length, 32);
});

it('e2e: shows correct number of findings for upgrading application customizer 1.4.1 project to 1.5.0', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-141-applicationcustomizer'));

await command.action(logger, { options: { toVersion: '1.5.0', output: 'json' } } as any);
const findings: FindingToReport[] = log[0];
assert.strictEqual(findings.length, 26);
assert.strictEqual(findings.length, 25);
});

it('e2e: shows correct number of findings for upgrading list view command set 1.4.1 project to 1.5.0', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-141-listviewcommandset'));

await command.action(logger, { options: { toVersion: '1.5.0', output: 'json' } } as any);
const findings: FindingToReport[] = log[0];
assert.strictEqual(findings.length, 26);
assert.strictEqual(findings.length, 25);
});

it('e2e: shows correct number of findings for upgrading field customizer react 1.4.1 project to 1.5.0', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-141-fieldcustomizer-react'));

await command.action(logger, { options: { toVersion: '1.5.0', output: 'json' } } as any);
const findings: FindingToReport[] = log[0];
assert.strictEqual(findings.length, 25);
assert.strictEqual(findings.length, 24);
});
//#endregion

Expand Down Expand Up @@ -2934,15 +2934,15 @@ describe(commands.PROJECT_UPGRADE, () => {

await command.action(logger, { options: { toVersion: '1.17.3', output: 'json' } } as any);
const findings: FindingToReport[] = log[0];
assert.strictEqual(findings.length, 10);
assert.strictEqual(findings.length, 9);
});

it('e2e: shows correct number of findings for upgrading form customizer react 1.17.2 project to 1.17.3', async () => {
sinon.stub(command as any, 'getProjectRoot').callsFake(_ => path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-1172-formcustomizer-react'));

await command.action(logger, { options: { toVersion: '1.17.3', output: 'json' } } as any);
const findings: FindingToReport[] = log[0];
assert.strictEqual(findings.length, 12);
assert.strictEqual(findings.length, 11);
});

it('e2e: shows correct number of findings for upgrading list view command set 1.17.2 project to 1.17.3', async () => {
Expand All @@ -2966,7 +2966,7 @@ describe(commands.PROJECT_UPGRADE, () => {

await command.action(logger, { options: { toVersion: '1.17.3', output: 'json' } } as any);
const findings: FindingToReport[] = log[0];
assert.strictEqual(findings.length, 14);
assert.strictEqual(findings.length, 13);
});

it('e2e: shows correct number of findings for upgrading web part with optional dependencies 1.17.2 project to 1.17.3', async () => {
Expand Down Expand Up @@ -3602,7 +3602,7 @@ describe(commands.PROJECT_UPGRADE, () => {

await command.action(logger, { options: { toVersion: '1.6.0', output: 'json' } } as any);
const findings: FindingToReport[] = log[0];
assert.strictEqual(findings.length, 32);
assert.strictEqual(findings.length, 31);
});
//#endregion

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,87 @@ describe('DependencyRule', () => {
assert.strictEqual(findings.length, 0);
});

it('returns notification when rule version is higher than open-ended range', () => {
const project: Project = {
path: '/usr/tmp',
packageJson: {
dependencies: {
'test-package': '>=0.5.0 <0.9.0'
},
devDependencies: {},
source: JSON.stringify({
dependencies: {
'test-package': '>=0.5.0 <0.9.0'
},
devDependencies: {}
}, null, 2)
}
};
depRule.visit(project, findings);
assert.strictEqual(findings.length, 1, 'Incorrect number of findings');
assert.strictEqual(findings[0].occurrences[0].position?.line, 3, 'Incorrect line number');
});

it('handles version range with multiple upper bounds correctly', () => {
const project: Project = {
path: '/usr/tmp',
packageJson: {
dependencies: {
'test-package': '>=0.2.0 <=0.3.0 || >=0.4.0 <=0.9.0'
},
devDependencies: {},
source: JSON.stringify({
dependencies: {
'test-package': '>=0.2.0 <=0.3.0 || >=0.4.0 <=0.9.0'
},
devDependencies: {}
}, null, 2)
}
};
depRule.visit(project, findings);
assert.strictEqual(findings.length, 1, 'Incorrect number of findings');
});

it('handles version range without upper bound correctly', () => {
const project: Project = {
path: '/usr/tmp',
packageJson: {
dependencies: {
'test-package': '>=1.5.0'
},
devDependencies: {},
source: JSON.stringify({
dependencies: {
'test-package': '>=1.5.0'
},
devDependencies: {}
}, null, 2)
}
};
depRule.visit(project, findings);
assert.strictEqual(findings.length, 0, 'Incorrect number of findings');
});

it('handles version range with only upper bounds correctly', () => {
const project: Project = {
path: '/usr/tmp',
packageJson: {
dependencies: {
'test-package': '<=0.9.0'
},
devDependencies: {},
source: JSON.stringify({
dependencies: {
'test-package': '<=0.9.0'
},
devDependencies: {}
}, null, 2)
}
};
depRule.visit(project, findings);
assert.strictEqual(findings.length, 1, 'Incorrect number of findings');
});

it('returns uninstall resolution for uninstall a dev dependency', () => {
const rule: DependencyRule = new DevDepRule2();
assert.strictEqual(rule.resolution, 'uninstallDev test-package');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { lt, valid, validRange } from 'semver';
import semver from 'semver';
import { Hash } from '../../../../../../utils/types.js';
import { JsonRule } from '../../JsonRule.js';
import { Project } from '../../project-model/index.js';
Expand Down Expand Up @@ -47,25 +47,15 @@ export abstract class DependencyRule extends JsonRule {

const projectDependencies: Hash | undefined = this.isDevDep ? project.packageJson.devDependencies : project.packageJson.dependencies;
const versionEntry: string | null = projectDependencies ? projectDependencies[this.packageName] : '';
const packageVersion: string | null = valid(versionEntry);
const versionRange: string | null = validRange(versionEntry);
if (this.add) {
let jsonProperty: string = this.isDevDep ? 'devDependencies' : 'dependencies';

if (versionEntry) {
jsonProperty += `.${this.packageName}`;

if (packageVersion) {
if (lt(packageVersion, this.packageVersion)) {
const node = this.getAstNodeFromFile(project.packageJson, jsonProperty);
this.addFindingWithPosition(findings, node);
}
}
else {
if (versionRange) {
const node = this.getAstNodeFromFile(project.packageJson, jsonProperty);
this.addFindingWithPosition(findings, node);
}
if (this.#needsUpdate(this.packageVersion, versionEntry)) {
const node = this.getAstNodeFromFile(project.packageJson, jsonProperty);
this.addFindingWithPosition(findings, node);
}
}
else {
Expand All @@ -88,4 +78,71 @@ export abstract class DependencyRule extends JsonRule {
}
}
}

/**
* Determines if a package needs to be updated based on a rule version
* @param {string} ruleVersion - The version/range from the rule (e.g., '5.8.1', '~5.8.0', '^6.0.0')
* @param {string} currentVersion - The version/range from package.json
* @returns {boolean} - true if update is needed
*/
#needsUpdate(ruleVersion: string, currentVersion: string): boolean {
try {
// Get minimum versions for both
const ruleMin = semver.minVersion(ruleVersion);
const currentMin = semver.minVersion(currentVersion);

// Check if ranges overlap
const rangesOverlap = semver.intersects(ruleVersion, currentVersion);

if (rangesOverlap) {
// Even if they overlap, update if rule requires a higher minimum version
if (ruleMin && currentMin && semver.gt(ruleMin, currentMin)) {
return true;
}
return false;
}

// Ranges don't overlap - check if rule range is greater
// Get the maximum version that satisfies the current range
const currentMax = this.#getMaxVersion(currentVersion);

// If rule's minimum is greater than current's maximum, update is needed
return !!(ruleMin && currentMax && semver.gt(ruleMin, currentMax));
}
catch {
return false;
}
}

/**
* Gets the maximum version from a range
* For open-ended ranges like '>=1.0.0', returns the minVersion
* For bounded ranges, returns the upper bound
*/
#getMaxVersion(range: string): semver.SemVer | null {
const rangeObj = new semver.Range(range);

// If it's a specific version (no range operators), return it
if (semver.valid(range)) {
return semver.parse(range);
}

// For ranges, get the highest version from the set
// Check the range set to find upper bounds
let maxVer = null;

for (const comparatorSet of rangeObj.set) {
for (const comparator of comparatorSet) {
if (comparator.operator === '<' || comparator.operator === '<=') {
const ver = comparator.semver;
if (!maxVer || semver.gt(ver, maxVer)) {
maxVer = ver;
}
}
}
}

// If no upper bound found, use minVersion as fallback
return maxVer || semver.minVersion(range);
}
}