Skip to content

Commit 4801dd2

Browse files
Keen Yee Liaukyliau
Keen Yee Liau
authored andcommitted
fix: use same logic to resolve typescript in banner and server
This commit refactors version_provider.ts to resolve a node module with any minimum version, not just major version. The same logic is now used in server.ts and banner.ts to resolve tsserver. This enables us to accurately report the version of the module require-d.
1 parent 06e3ced commit 4801dd2

File tree

4 files changed

+174
-60
lines changed

4 files changed

+174
-60
lines changed

server/src/banner.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {parseCommandLine} from './cmdline_utils';
2+
import {resolveTsServer} from './version_provider';
23

34
/**
45
* This method provides a custom implementation for the AMD loader to resolve
@@ -7,17 +8,14 @@ import {parseCommandLine} from './cmdline_utils';
78
* @param cb function to invoke with resolved modules
89
*/
910
export function define(modules: string[], cb: (...modules: any[]) => void) {
10-
function resolve(packageName: string, paths: string[]) {
11-
try {
12-
return require.resolve(packageName, {paths});
13-
} catch {
14-
}
15-
}
1611
const TSSERVER = 'typescript/lib/tsserverlibrary';
1712
const resolvedModules = modules.map(m => {
18-
if (m === TSSERVER || m === 'typescript') {
13+
if (m === 'typescript') {
14+
throw new Error(`Import '${TSSERVER}' instead of 'typescript'`);
15+
}
16+
if (m === TSSERVER) {
1917
const {tsProbeLocations} = parseCommandLine(process.argv);
20-
m = resolve(TSSERVER, tsProbeLocations) || TSSERVER;
18+
m = resolveTsServer(tsProbeLocations).resolvedPath;
2119
}
2220
return require(m);
2321
});

server/src/server.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {generateHelpMessage, parseCommandLine} from './cmdline_utils';
1010
import {createLogger} from './logger';
1111
import {ServerHost} from './server_host';
1212
import {Session} from './session';
13-
import {resolveWithMinMajor} from './version_provider';
13+
import {resolveNgLangSvc, resolveTsServer} from './version_provider';
1414

1515
// Parse command line arguments
1616
const options = parseCommandLine(process.argv);
@@ -26,9 +26,8 @@ const logger = createLogger({
2626
logVerbosity: options.logVerbosity,
2727
});
2828

29-
const {tsProbeLocations, ngProbeLocations} = options;
30-
const ts = resolveWithMinMajor('typescript', 3, tsProbeLocations);
31-
const ng = resolveWithMinMajor('@angular/language-service', 9, ngProbeLocations);
29+
const ts = resolveTsServer(options.tsProbeLocations);
30+
const ng = resolveNgLangSvc(options.ngProbeLocations);
3231

3332
// ServerHost provides native OS functionality
3433
const host = new ServerHost();

server/src/tests/version_provider_spec.ts

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,69 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {resolveWithMinMajor} from '../version_provider';
9+
import {resolveNgLangSvc, resolveTsServer, Version} from '../version_provider';
1010

11-
describe('resolveWithMinMajor', () => {
11+
describe('Node Module Resolver', () => {
1212
const probeLocations = [__dirname];
1313

14-
it('should find typescript >= v2', () => {
15-
const result = resolveWithMinMajor('typescript', 2, probeLocations);
16-
expect(result.version).toBe('3.6.4');
14+
it('should be able to resolve tsserver', () => {
15+
const result = resolveTsServer(probeLocations);
16+
expect(result).toBeDefined();
17+
expect(result.resolvedPath).toMatch(/typescript\/lib\/tsserverlibrary.js$/);
1718
});
1819

19-
it('should find typescript v3', () => {
20-
const result = resolveWithMinMajor('typescript', 3, probeLocations);
21-
expect(result.version).toBe('3.6.4');
20+
it('should be able to resolve Angular language service', () => {
21+
const result = resolveNgLangSvc(probeLocations);
22+
expect(result).toBeDefined();
23+
expect(result.resolvedPath).toMatch(/language-service.umd.js$/);
2224
});
25+
});
26+
27+
describe('Version', () => {
28+
it('should parse version string correctly', () => {
29+
const cases: Array<[string, number, number, number]> = [
30+
// version string | major | minor | patch
31+
['1', 1, 0, 0],
32+
['1.2', 1, 2, 0],
33+
['1.2.3', 1, 2, 3],
34+
['9.0.0-rc.1+126.sha-0c38aae.with-local-changes', 9, 0, 0],
35+
];
36+
for (const [versionStr, major, minor, patch] of cases) {
37+
const v = new Version(versionStr);
38+
expect(v.major).toBe(major);
39+
expect(v.minor).toBe(minor);
40+
expect(v.patch).toBe(patch);
41+
}
42+
});
43+
44+
it('should compare versions correctly', () => {
45+
const cases: Array<[string, string, boolean]> = [
46+
// lhs | rhs | result
47+
['1', '1', true],
48+
['1', '2', false],
49+
['2', '2.0', true],
50+
['2', '2.1', false],
51+
['2', '2.0.0', true],
52+
['2', '2.0.1', false],
53+
54+
['1.2', '1', true],
55+
['1.2', '2', false],
56+
['2.2', '2.1', true],
57+
['2.2', '2.7', false],
58+
['3.2', '3.2.0', true],
59+
['3.2', '3.2.1', false],
2360

24-
it('should fail to find typescript v4', () => {
25-
expect(() => resolveWithMinMajor('typescript', 4, probeLocations))
26-
.toThrowError(/^Failed to resolve 'typescript'/);
61+
['1.2.3', '1', true],
62+
['1.2.3', '2', false],
63+
['2.2.3', '2.1', true],
64+
['2.2.3', '2.3', false],
65+
['3.2.3', '3.2.2', true],
66+
['3.2.3', '3.2.4', false],
67+
];
68+
for (const [s1, s2, result] of cases) {
69+
const v1 = new Version(s1);
70+
const v2 = new Version(s2);
71+
expect(v1.greaterThanOrEqual(v2)).toBe(result, `Expect ${v1} >= ${v2}`);
72+
}
2773
});
2874
});

server/src/version_provider.ts

Lines changed: 108 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,65 +6,136 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import * as path from 'path';
9+
const MIN_TS_VERSION = '3.6';
10+
const MIN_NG_VERSION = '9.0';
1011

1112
/**
1213
* Represents a valid node module that has been successfully resolved.
1314
*/
14-
export interface NodeModule {
15+
interface NodeModule {
1516
resolvedPath: string;
16-
version?: string;
17+
version: Version;
1718
}
1819

19-
function resolve(packageName: string, paths: string[]): NodeModule|undefined {
20+
function resolve(packageName: string, location: string, rootPackage?: string): NodeModule|
21+
undefined {
22+
rootPackage = rootPackage || packageName;
2023
try {
21-
// Here, use native NodeJS require instead of ServerHost.require because
22-
// we want the full path of the resolution provided by native
23-
// `require.resolve()`, which ServerHost does not provide.
24-
const resolvedPath = require.resolve(`${packageName}/package.json`, {paths});
25-
const packageJson = require(resolvedPath);
24+
const packageJsonPath = require.resolve(`${rootPackage}/package.json`, {
25+
paths: [location],
26+
});
27+
const packageJson = require(packageJsonPath);
28+
const resolvedPath = require.resolve(packageName, {
29+
paths: [location],
30+
});
2631
return {
27-
resolvedPath: path.dirname(resolvedPath),
28-
version: packageJson.version,
32+
resolvedPath,
33+
version: new Version(packageJson.version),
2934
};
3035
} catch {
3136
}
3237
}
3338

34-
function minVersion(nodeModule: NodeModule, minMajor: number): boolean {
35-
if (!nodeModule.version) {
36-
return false;
37-
}
38-
const [majorStr] = nodeModule.version.split('.');
39-
if (!majorStr) {
40-
return false;
39+
/**
40+
* Resolve the node module with the specified `packageName` that satisfies
41+
* the specified minimum version.
42+
* @param packageName name of package to be resolved
43+
* @param minVersionStr minimum version
44+
* @param probeLocations locations to initiate node module resolution
45+
* @param rootPackage location of package.json, if different from `packageName`
46+
*/
47+
function resolveWithMinVersion(
48+
packageName: string, minVersionStr: string, probeLocations: string[],
49+
rootPackage?: string): NodeModule {
50+
if (rootPackage && !packageName.startsWith(rootPackage)) {
51+
throw new Error(`${packageName} must be in the root package`);
4152
}
42-
const major = Number(majorStr);
43-
if (isNaN(major)) {
44-
return false;
53+
const minVersion = new Version(minVersionStr);
54+
for (const location of probeLocations) {
55+
const nodeModule = resolve(packageName, location, rootPackage);
56+
if (nodeModule && nodeModule.version.greaterThanOrEqual(minVersion)) {
57+
return nodeModule;
58+
}
4559
}
46-
return major >= minMajor;
60+
throw new Error(
61+
`Failed to resolve '${packageName}' with minimum version '${minVersion}' from ` +
62+
JSON.stringify(probeLocations, null, 2));
4763
}
4864

4965
/**
50-
* Resolve the node module with the specified `packageName` that satisfies
51-
* the specified minimum major version.
52-
* @param packageName
53-
* @param minMajor
66+
* Resolve `typescript/lib/tsserverlibrary` from the given locations.
5467
* @param probeLocations
5568
*/
56-
export function resolveWithMinMajor(
57-
packageName: string, minMajor: number, probeLocations: string[]): NodeModule {
58-
for (const location of probeLocations) {
59-
const nodeModule = resolve(packageName, [location]);
60-
if (!nodeModule) {
61-
continue;
69+
export function resolveTsServer(probeLocations: string[]): NodeModule {
70+
const tsserver = 'typescript/lib/tsserverlibrary';
71+
return resolveWithMinVersion(tsserver, MIN_TS_VERSION, probeLocations, 'typescript');
72+
}
73+
74+
/**
75+
* Resolve `@angular/language-service` from the given locations.
76+
* @param probeLocations
77+
*/
78+
export function resolveNgLangSvc(probeLocations: string[]): NodeModule {
79+
const nglangsvc = '@angular/language-service';
80+
return resolveWithMinVersion(nglangsvc, MIN_NG_VERSION, probeLocations);
81+
}
82+
83+
/**
84+
* Converts the specified string `a` to non-negative integer.
85+
* Returns -1 if the result is NaN.
86+
* @param a
87+
*/
88+
function parseNonNegativeInt(a: string): number {
89+
// parseInt() will try to convert as many as possible leading characters that
90+
// are digits. This means a string like "123abc" will be converted to 123.
91+
// For our use case, this is sufficient.
92+
const i = parseInt(a, 10 /* radix */);
93+
return isNaN(i) ? -1 : i;
94+
}
95+
96+
export class Version {
97+
readonly major: number;
98+
readonly minor: number;
99+
readonly patch: number;
100+
101+
constructor(private readonly versionStr: string) {
102+
const [major, minor, patch] = Version.parseVersionStr(versionStr);
103+
this.major = major;
104+
this.minor = minor;
105+
this.patch = patch;
106+
}
107+
108+
greaterThanOrEqual(other: Version): boolean {
109+
if (this.major < other.major) {
110+
return false;
62111
}
63-
if (minVersion(nodeModule, minMajor)) {
64-
return nodeModule;
112+
if (this.major > other.major) {
113+
return true;
65114
}
115+
if (this.minor < other.minor) {
116+
return false;
117+
}
118+
if (this.minor > other.minor) {
119+
return true;
120+
}
121+
return this.patch >= other.patch;
122+
}
123+
124+
toString(): string {
125+
return this.versionStr;
126+
}
127+
128+
/**
129+
* Converts the specified `versionStr` to its number constituents. Invalid
130+
* number value is represented as negative number.
131+
* @param versionStr
132+
*/
133+
static parseVersionStr(versionStr: string): [number, number, number] {
134+
const [major, minor, patch] = versionStr.split('.').map(parseNonNegativeInt);
135+
return [
136+
major === undefined ? 0 : major,
137+
minor === undefined ? 0 : minor,
138+
patch === undefined ? 0 : patch,
139+
];
66140
}
67-
throw new Error(
68-
`Failed to resolve '${packageName}' with minimum major version '${minMajor}' from ` +
69-
JSON.stringify(probeLocations, null, 2));
70141
}

0 commit comments

Comments
 (0)