|
6 | 6 | * found in the LICENSE file at https://angular.io/license
|
7 | 7 | */
|
8 | 8 |
|
9 |
| -import * as path from 'path'; |
| 9 | +const MIN_TS_VERSION = '3.6'; |
| 10 | +const MIN_NG_VERSION = '9.0'; |
10 | 11 |
|
11 | 12 | /**
|
12 | 13 | * Represents a valid node module that has been successfully resolved.
|
13 | 14 | */
|
14 |
| -export interface NodeModule { |
| 15 | +interface NodeModule { |
15 | 16 | resolvedPath: string;
|
16 |
| - version?: string; |
| 17 | + version: Version; |
17 | 18 | }
|
18 | 19 |
|
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; |
20 | 23 | 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 | + }); |
26 | 31 | return {
|
27 |
| - resolvedPath: path.dirname(resolvedPath), |
28 |
| - version: packageJson.version, |
| 32 | + resolvedPath, |
| 33 | + version: new Version(packageJson.version), |
29 | 34 | };
|
30 | 35 | } catch {
|
31 | 36 | }
|
32 | 37 | }
|
33 | 38 |
|
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`); |
41 | 52 | }
|
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 | + } |
45 | 59 | }
|
46 |
| - return major >= minMajor; |
| 60 | + throw new Error( |
| 61 | + `Failed to resolve '${packageName}' with minimum version '${minVersion}' from ` + |
| 62 | + JSON.stringify(probeLocations, null, 2)); |
47 | 63 | }
|
48 | 64 |
|
49 | 65 | /**
|
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. |
54 | 67 | * @param probeLocations
|
55 | 68 | */
|
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 | + return resolveWithMinVersion('@angular/language-service', MIN_NG_VERSION, probeLocations); |
| 80 | +} |
| 81 | + |
| 82 | +/** |
| 83 | + * Converts the specified string `a` to integer. Returns -1 if the result is NaN. |
| 84 | + * @param a |
| 85 | + */ |
| 86 | +function atoi(a: string): number { |
| 87 | + // parseInt() will try to convert as many as possible leading characters that |
| 88 | + // are digits. This means a string like "123abc" will be converted to 123. |
| 89 | + // For our use case, this is sufficient. |
| 90 | + const i = parseInt(a, 10 /* radix */); |
| 91 | + return isNaN(i) ? -1 : i; |
| 92 | +} |
| 93 | + |
| 94 | +export class Version { |
| 95 | + readonly major: number; |
| 96 | + readonly minor: number; |
| 97 | + readonly patch: number; |
| 98 | + |
| 99 | + constructor(private readonly versionStr: string) { |
| 100 | + const [major, minor, patch] = Version.parseVersionStr(versionStr); |
| 101 | + this.major = major; |
| 102 | + this.minor = minor; |
| 103 | + this.patch = patch; |
| 104 | + } |
| 105 | + |
| 106 | + greaterThanOrEqual(other: Version): boolean { |
| 107 | + if (this.major < other.major) { |
| 108 | + return false; |
62 | 109 | }
|
63 |
| - if (minVersion(nodeModule, minMajor)) { |
64 |
| - return nodeModule; |
| 110 | + if (this.major > other.major) { |
| 111 | + return true; |
65 | 112 | }
|
| 113 | + if (this.minor < other.minor) { |
| 114 | + return false; |
| 115 | + } |
| 116 | + if (this.minor > other.minor) { |
| 117 | + return true; |
| 118 | + } |
| 119 | + return this.patch >= other.patch; |
| 120 | + } |
| 121 | + |
| 122 | + toString(): string { |
| 123 | + return this.versionStr; |
| 124 | + } |
| 125 | + |
| 126 | + /** |
| 127 | + * Converts the specified `versionStr` to its number constituents. Invalid |
| 128 | + * number value is represented as negative number. |
| 129 | + * @param versionStr |
| 130 | + */ |
| 131 | + static parseVersionStr(versionStr: string): [number, number, number] { |
| 132 | + const [major, minor, patch] = versionStr.split('.').map(atoi); |
| 133 | + return [ |
| 134 | + major === undefined ? 0 : major, |
| 135 | + minor === undefined ? 0 : minor, |
| 136 | + patch === undefined ? 0 : patch, |
| 137 | + ]; |
66 | 138 | }
|
67 |
| - throw new Error( |
68 |
| - `Failed to resolve '${packageName}' with minimum major version '${minMajor}' from ` + |
69 |
| - JSON.stringify(probeLocations, null, 2)); |
70 | 139 | }
|
0 commit comments