forked from microsoft/rushstack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathImport.ts
More file actions
323 lines (298 loc) · 11.2 KB
/
Copy pathImport.ts
File metadata and controls
323 lines (298 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'path';
import importLazy = require('import-lazy');
import * as Resolve from 'resolve';
import nodeModule = require('module');
import { PackageJsonLookup } from './PackageJsonLookup';
import { FileSystem } from './FileSystem';
import { IPackageJson } from './IPackageJson';
/**
* Common options shared by {@link IImportResolveModuleOptions} and {@link IImportResolvePackageOptions}
* @public
*/
export interface IImportResolveOptions {
/**
* The path from which {@link IImportResolveModuleOptions.modulePath} or
* {@link IImportResolvePackageOptions.packageName} should be resolved.
*/
baseFolderPath: string;
/**
* If true, if the package name matches a Node.js system module, then the return
* value will be the package name without any path.
*
* @remarks
* This will take precedence over an installed NPM package of the same name.
*
* Example:
* ```ts
* // Returns the string "fs" indicating the Node.js system module
* Import.resolveModulePath({
* resolvePath: "fs",
* basePath: process.cwd()
* })
* ```
*/
includeSystemModules?: boolean;
/**
* If true, then resolvePath is allowed to refer to the package.json of the active project.
*
* @remarks
* This will take precedence over any installed dependency with the same name.
* Note that this requires an additional PackageJsonLookup calculation.
*
* Example:
* ```ts
* // Returns an absolute path to the current package
* Import.resolveModulePath({
* resolvePath: "current-project",
* basePath: process.cwd(),
* allowSelfReference: true
* })
* ```
*/
allowSelfReference?: boolean;
}
/**
* Options for {@link Import.resolveModule}
* @public
*/
export interface IImportResolveModuleOptions extends IImportResolveOptions {
/**
* The module identifier to resolve. For example "\@rushstack/node-core-library" or
* "\@rushstack/node-core-library/lib/index.js"
*/
modulePath: string;
}
/**
* Options for {@link Import.resolvePackage}
* @public
*/
export interface IImportResolvePackageOptions extends IImportResolveOptions {
/**
* The package name to resolve. For example "\@rushstack/node-core-library"
*/
packageName: string;
}
interface IPackageDescriptor {
packageRootPath: string;
packageName: string;
}
/**
* Helpers for resolving and importing Node.js modules.
* @public
*/
export class Import {
private static __builtInModules: Set<string> | undefined;
private static get _builtInModules(): Set<string> {
if (!Import.__builtInModules) {
Import.__builtInModules = new Set<string>(nodeModule.builtinModules);
}
return Import.__builtInModules;
}
/**
* Provides a way to improve process startup times by lazy-loading imported modules.
*
* @remarks
* This is a more structured wrapper for the {@link https://www.npmjs.com/package/import-lazy|import-lazy}
* package. It enables you to replace an import like this:
*
* ```ts
* import * as example from 'example'; // <-- 100ms load time
*
* if (condition) {
* example.doSomething();
* }
* ```
*
* ...with a pattern like this:
*
* ```ts
* const example: typeof import('example') = Import.lazy('example', require);
*
* if (condition) {
* example.doSomething(); // <-- 100ms load time occurs here, only if needed
* }
* ```
*
* The implementation relies on JavaScript's `Proxy` feature to intercept access to object members. Thus
* it will only work correctly with certain types of module exports. If a particular export isn't well behaved,
* you may need to find (or introduce) some other module in your dependency graph to apply the optimization to.
*
* Usage guidelines:
*
* - Always specify types using `typeof` as shown above.
*
* - Never apply lazy-loading in a way that would convert the module's type to `any`. Losing type safety
* seriously impacts the maintainability of the code base.
*
* - In cases where the non-runtime types are needed, import them separately using the `Types` suffix:
*
* ```ts
* const example: typeof import('example') = Import.lazy('example', require);
* import type * as exampleTypes from 'example';
* ```
*
* - If the imported module confusingly has the same name as its export, then use the Module suffix:
*
* ```ts
* const exampleModule: typeof import('../../logic/Example') = Import.lazy(
* '../../logic/Example', require);
* import type * as exampleTypes from '../../logic/Example';
* ```
*
* - If the exports cause a lot of awkwardness (e.g. too many expressions need to have `exampleModule.` inserted
* into them), or if some exports cannot be proxied (e.g. `Import.lazy('example', require)` returns a function
* signature), then do not lazy-load that module. Instead, apply lazy-loading to some other module which is
* better behaved.
*
* - It's recommended to sort imports in a standard ordering:
*
* ```ts
* // 1. external imports
* import * as path from 'path';
* import { Import, JsonFile, JsonObject } from '@rushstack/node-core-library';
*
* // 2. local imports
* import { LocalFile } from './path/LocalFile';
*
* // 3. lazy-imports (which are technically variables, not imports)
* const semver: typeof import('semver') = Import.lazy('semver', require);
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public static lazy(moduleName: string, require: (id: string) => unknown): any {
const importLazyLocal: (moduleName: string) => unknown = importLazy(require);
return importLazyLocal(moduleName);
}
/**
* This resolves a module path using similar logic as the Node.js `require.resolve()` API,
* but supporting extra features such as specifying the base folder.
*
* @remarks
* A module path is a text string that might appear in a statement such as
* `import { X } from "____";` or `const x = require("___");`. The implementation is based
* on the popular `resolve` NPM package.
*
* Suppose `example` is an NPM package whose entry point is `lib/index.js`:
* ```ts
* // Returns "/path/to/project/node_modules/example/lib/index.js"
* Import.resolveModule({ modulePath: 'example' });
*
* // Returns "/path/to/project/node_modules/example/lib/other.js"
* Import.resolveModule({ modulePath: 'example/lib/other' });
* ```
* If you need to determine the containing package folder
* (`/path/to/project/node_modules/example`), use {@link Import.resolvePackage} instead.
*
* @returns the absolute path of the resolved module.
* If {@link IImportResolveOptions.includeSystemModules} is specified
* and a system module is found, then its name is returned without any file path.
*/
public static resolveModule(options: IImportResolveModuleOptions): string {
const { modulePath } = options;
if (path.isAbsolute(modulePath)) {
return modulePath;
}
const normalizedRootPath: string = FileSystem.getRealPath(options.baseFolderPath);
if (modulePath.startsWith('.')) {
// This looks like a conventional relative path
return path.resolve(normalizedRootPath, modulePath);
}
if (options.includeSystemModules === true && Import._builtInModules.has(modulePath)) {
return modulePath;
}
if (options.allowSelfReference === true) {
const ownPackage: IPackageDescriptor | undefined = Import._getPackageName(options.baseFolderPath);
if (ownPackage && modulePath.startsWith(ownPackage.packageName)) {
const packagePath: string = modulePath.substr(ownPackage.packageName.length + 1);
return path.resolve(ownPackage.packageRootPath, packagePath);
}
}
try {
return Resolve.sync(
// Append a slash to the package name to ensure `resolve.sync` doesn't attempt to return a system package
options.includeSystemModules !== true && modulePath.indexOf('/') === -1
? `${modulePath}/`
: modulePath,
{
basedir: normalizedRootPath,
preserveSymlinks: false
}
);
} catch (e) {
throw new Error(`Cannot find module "${modulePath}" from "${options.baseFolderPath}".`);
}
}
/**
* Performs module resolution to determine the folder where a package is installed.
*
* @remarks
* Suppose `example` is an NPM package whose entry point is `lib/index.js`:
* ```ts
* // Returns "/path/to/project/node_modules/example"
* Import.resolvePackage({ packageName: 'example' });
* ```
*
* If you need to resolve a module path, use {@link Import.resolveModule} instead:
* ```ts
* // Returns "/path/to/project/node_modules/example/lib/index.js"
* Import.resolveModule({ modulePath: 'example' });
* ```
*
* @returns the absolute path of the package folder.
* If {@link IImportResolveOptions.includeSystemModules} is specified
* and a system module is found, then its name is returned without any file path.
*/
public static resolvePackage(options: IImportResolvePackageOptions): string {
const { packageName } = options;
if (options.includeSystemModules && Import._builtInModules.has(packageName)) {
return packageName;
}
const normalizedRootPath: string = FileSystem.getRealPath(options.baseFolderPath);
if (options.allowSelfReference) {
const ownPackage: IPackageDescriptor | undefined = Import._getPackageName(options.baseFolderPath);
if (ownPackage && ownPackage.packageName === packageName) {
return ownPackage.packageRootPath;
}
}
try {
const resolvedPath: string = Resolve.sync(packageName, {
basedir: normalizedRootPath,
preserveSymlinks: false,
packageFilter: (pkg: { main: string }): { main: string } => {
// Hardwire "main" to point to a file that is guaranteed to exist.
// This helps resolve packages such as @types/node that have no entry point.
// And then we can use path.dirname() below to locate the package folder,
// even if the real entry point was in an subfolder with arbitrary nesting.
pkg.main = 'package.json';
return pkg;
}
});
const packagePath: string = path.dirname(resolvedPath);
const packageJson: IPackageJson = PackageJsonLookup.instance.loadPackageJson(
path.join(packagePath, 'package.json')
);
if (packageJson.name === packageName) {
return packagePath;
} else {
throw new Error();
}
} catch (e) {
throw new Error(`Cannot find package "${packageName}" from "${options.baseFolderPath}".`);
}
}
private static _getPackageName(rootPath: string): IPackageDescriptor | undefined {
const packageJsonPath: string | undefined =
PackageJsonLookup.instance.tryGetPackageJsonFilePathFor(rootPath);
if (packageJsonPath) {
const packageJson: IPackageJson = PackageJsonLookup.instance.loadPackageJson(packageJsonPath);
return {
packageRootPath: path.dirname(packageJsonPath),
packageName: packageJson.name
};
} else {
return undefined;
}
}
}