Skip to content

Commit 9bfef1a

Browse files
fix(sys): tweak NodeLazyRequire logic around too-high-versions errors (#3347)
this changes the logic that we use in the `NodeLazyRequire` class to error out in fewer situations. the changes are: - the data structure for specifying our version ranges is changed from `[string, string]` (`[minVersion, recommendedVersion]`) to an object with `minVersion` `recommendedVersion` required properties and an optional `maxVersion` property. - in the `ensure` method on `NodeLazyRequire` we check if `maxVersion` is defined on a given version range requirement. - If so, we check that the installed version is greater than or equal to `minVersion` and less than the major version of `maxVersion`. - If not, we just check that `minVersion <= installedVersion` this should give us the flexibility to mark certain versions of packages as incompatible (for instance jest@28) without having to do that for all packages that we lazy require. this is helpful because for most of them we just want to set a minimum version and don't have a need for an enforced maximum version.
1 parent b7adc33 commit 9bfef1a

File tree

3 files changed

+75
-40
lines changed

3 files changed

+75
-40
lines changed

src/sys/node/node-lazy-require.ts

+39-21
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,34 @@ import fs from 'graceful-fs';
55
import path from 'path';
66
import satisfies from 'semver/functions/satisfies';
77
import major from 'semver/functions/major';
8+
import semverLte from 'semver/functions/lte';
89

910
/**
10-
* The version range that we support for a given package
11-
* [0] is the lower end, while [1] is the higher end.
12-
*
13-
* These strings should be standard semver strings.
11+
* The version range that we support for a given package. The strings should be
12+
* standard semver strings.
1413
*/
15-
type NodeVersionRange = [string, string];
14+
interface DepVersionRange {
15+
minVersion: string;
16+
recommendedVersion: string;
17+
/**
18+
* Max version is optional because we aren't always worried about upgrades.
19+
* This should be set for packages where major version upgrades have
20+
* historically caused problems, or when we've identified a specific issue
21+
* that requires us to stay at or below a certain version. Note that
22+
* `NodeLazyRequire.ensure` only checks the major version.
23+
*/
24+
maxVersion?: string;
25+
}
1626

1727
/**
18-
* A manifest for lazily-loaded dependencies, mapping dependency names
19-
* to version ranges.
28+
* A manifest for lazily-loaded dependencies, mapping dependency names to
29+
* version ranges.
2030
*/
21-
type LazyDependencies = Record<string, NodeVersionRange>;
31+
export type LazyDependencies = Record<string, DepVersionRange>;
2232

2333
/**
24-
* Lazy requirer for Node, with functionality for specifying version ranges
25-
* and returning diagnostic errors if requirements aren't met.
34+
* Lazy requirer for Node, with functionality for specifying version ranges and
35+
* returning diagnostic errors if requirements aren't met.
2636
*/
2737
export class NodeLazyRequire implements d.LazyRequire {
2838
private ensured = new Set<string>();
@@ -36,36 +46,44 @@ export class NodeLazyRequire implements d.LazyRequire {
3646
constructor(private nodeResolveModule: NodeResolveModule, private lazyDependencies: LazyDependencies) {}
3747

3848
/**
39-
* Ensure that a dependency within our supported range is installed in the current
40-
* environment. This function will check all the dependency requirements passed in when
41-
* the class is instantiated and return diagnostics if there are any issues.
49+
* Ensure that a dependency within our supported range is installed in the
50+
* current environment. This function will check all the dependency
51+
* requirements passed in when the class is instantiated and return
52+
* diagnostics if there are any issues.
4253
*
43-
* @param fromDir the directory from which we'll attempt to resolve the dependencies, typically
44-
* this will be project's root directory.
45-
* @param ensureModuleIds an array of module names whose versions we're going to check
46-
* @returns a Promise holding diagnostics if any of the dependencies either were not
47-
* resolved _or_ did not meet our version requirements.
54+
* @param fromDir the directory from which we'll attempt to resolve the
55+
* dependencies, typically this will be project's root directory.
56+
* @param ensureModuleIds an array of module names whose versions we're going
57+
* to check
58+
* @returns a Promise holding diagnostics if any of the dependencies either
59+
* were not resolved _or_ did not meet our version requirements.
4860
*/
4961
async ensure(fromDir: string, ensureModuleIds: string[]): Promise<d.Diagnostic[]> {
5062
const diagnostics: d.Diagnostic[] = [];
5163
const problemDeps: string[] = [];
5264

5365
ensureModuleIds.forEach((ensureModuleId) => {
5466
if (!this.ensured.has(ensureModuleId)) {
55-
const [minVersion, maxVersion] = this.lazyDependencies[ensureModuleId];
67+
const { minVersion, recommendedVersion, maxVersion } = this.lazyDependencies[ensureModuleId];
5668

5769
try {
5870
const pkgJsonPath = this.nodeResolveModule.resolveModule(fromDir, ensureModuleId);
5971
const installedPkgJson: d.PackageJsonData = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
6072

61-
if (satisfies(installedPkgJson.version, `${minVersion} - ${major(maxVersion)}.x`)) {
73+
const installedVersionIsGood = maxVersion
74+
? // if maxVersion, check that `minVersion <= installedVersion <= maxVersion`
75+
satisfies(installedPkgJson.version, `${minVersion} - ${major(maxVersion)}.x`)
76+
: // else, just check that `minVersion <= installedVersion`
77+
semverLte(minVersion, installedPkgJson.version);
78+
79+
if (installedVersionIsGood) {
6280
this.ensured.add(ensureModuleId);
6381
return;
6482
}
6583
} catch (e) {}
6684
// if we get here we didn't get to the `return` above, so either 1) there was some error
6785
// reading the package.json or 2) the version wasn't in our specified version range.
68-
problemDeps.push(`${ensureModuleId}@${maxVersion}`);
86+
problemDeps.push(`${ensureModuleId}@${recommendedVersion}`);
6987
}
7088
});
7189

src/sys/node/node-sys.ts

+7-8
Original file line numberDiff line numberDiff line change
@@ -588,14 +588,13 @@ export function createNodeSys(c: { process?: any } = {}) {
588588
const nodeResolve = new NodeResolveModule();
589589

590590
sys.lazyRequire = new NodeLazyRequire(nodeResolve, {
591-
// [minimumVersion, recommendedVersion]
592-
'@types/jest': ['24.9.1', '27.0.3'],
593-
jest: ['24.9.0', '27.4.5'],
594-
'jest-cli': ['24.9.0', '27.4.5'],
595-
pixelmatch: ['4.0.2', '4.0.2'],
596-
puppeteer: ['1.19.0', '10.0.0'],
597-
'puppeteer-core': ['1.19.0', '5.2.1'],
598-
'workbox-build': ['4.3.1', '4.3.1'],
591+
'@types/jest': { minVersion: '24.9.1', recommendedVersion: '27.0.3', maxVersion: '27.0.0' },
592+
jest: { minVersion: '24.9.1', recommendedVersion: '27.0.3', maxVersion: '27.0.0' },
593+
'jest-cli': { minVersion: '24.9.0', recommendedVersion: '27.4.5', maxVersion: '27.0.0' },
594+
pixelmatch: { minVersion: '4.0.2', recommendedVersion: '4.0.2' },
595+
puppeteer: { minVersion: '1.19.0', recommendedVersion: '10.0.0' },
596+
'puppeteer-core': { minVersion: '1.19.0', recommendedVersion: '5.2.1' },
597+
'workbox-build': { minVersion: '4.3.1', recommendedVersion: '4.3.1' },
599598
});
600599

601600
prcs.on('SIGINT', runInterruptsCallbacks);

src/sys/node/test/node-lazy-require.spec.ts

+29-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { NodeLazyRequire } from '../node-lazy-require';
1+
import { LazyDependencies, NodeLazyRequire } from '../node-lazy-require';
22
import { buildError } from '@utils';
33
import { NodeResolveModule } from '../node-resolve-module';
44
import fs from 'graceful-fs';
@@ -21,45 +21,63 @@ describe('node-lazy-require', () => {
2121
readFSMock.mockClear();
2222
});
2323

24-
function setup() {
24+
const jestTestRange = (maxVersion = '38.0.1'): LazyDependencies => ({
25+
jest: {
26+
minVersion: '2.0.7',
27+
recommendedVersion: '36.0.1',
28+
maxVersion,
29+
},
30+
});
31+
32+
function setup(versionRange: LazyDependencies) {
2533
const resolveModule = new NodeResolveModule();
26-
const nodeLazyRequire = new NodeLazyRequire(resolveModule, {
27-
jest: ['2.0.7', '38.0.1'],
28-
});
34+
const nodeLazyRequire = new NodeLazyRequire(resolveModule, versionRange);
2935
return nodeLazyRequire;
3036
}
3137

3238
it.each(['2.0.7', '10.10.10', '38.0.1', '38.0.2', '38.5.17'])(
3339
'should not error if installed package has a suitable major version (%p)',
3440
async (testVersion) => {
35-
const nodeLazyRequire = setup();
41+
const nodeLazyRequire = setup(jestTestRange());
42+
readFSMock.mockReturnValue(mockPackageJson(testVersion));
43+
let diagnostics = await nodeLazyRequire.ensure('.', ['jest']);
44+
expect(diagnostics.length).toBe(0);
45+
}
46+
);
47+
48+
it.each(['2.0.7', '10.10.10', '36.0.1', '38.0.2', '38.5.17'])(
49+
'should never error with versions above minVersion if there is no maxVersion supplied (%p)',
50+
async (testVersion) => {
51+
const nodeLazyRequire = setup(jestTestRange(undefined));
3652
readFSMock.mockReturnValue(mockPackageJson(testVersion));
3753
let diagnostics = await nodeLazyRequire.ensure('.', ['jest']);
3854
expect(diagnostics.length).toBe(0);
3955
}
4056
);
4157

42-
it('should error if the installed version of a package is too low', async () => {
43-
const nodeLazyRequire = setup();
58+
it.each(['38', undefined])('should error w/ installed version too low and maxVersion=%p', async (maxVersion) => {
59+
const range = jestTestRange(maxVersion);
60+
const nodeLazyRequire = setup(range);
4461
readFSMock.mockReturnValue(mockPackageJson('1.1.1'));
4562
let [error] = await nodeLazyRequire.ensure('.', ['jest']);
4663
expect(error).toEqual({
4764
...buildError([]),
4865
header: 'Please install supported versions of dev dependencies with either npm or yarn.',
49-
messageText: 'npm install --save-dev jest@38.0.1',
66+
messageText: `npm install --save-dev jest@${range.jest.recommendedVersion}`,
5067
});
5168
});
5269

5370
it.each(['100.1.1', '38.0.1-alpha.0'])(
5471
'should error if the installed version of a package is too high (%p)',
5572
async (version) => {
56-
const nodeLazyRequire = setup();
73+
const range = jestTestRange();
74+
const nodeLazyRequire = setup(range);
5775
readFSMock.mockReturnValue(mockPackageJson(version));
5876
let [error] = await nodeLazyRequire.ensure('.', ['jest']);
5977
expect(error).toEqual({
6078
...buildError([]),
6179
header: 'Please install supported versions of dev dependencies with either npm or yarn.',
62-
messageText: 'npm install --save-dev jest@38.0.1',
80+
messageText: `npm install --save-dev jest@${range.jest.recommendedVersion}`,
6381
});
6482
}
6583
);

0 commit comments

Comments
 (0)