forked from microsoft/TypeScript
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjsTyping.ts
472 lines (430 loc) · 19 KB
/
jsTyping.ts
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
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
import {
CharacterCodes,
combinePaths,
compareStringsCaseSensitive,
CompilerOptions,
Debug,
deduplicate,
equateStringsCaseSensitive,
Extension,
fileExtensionIs,
flatMap,
forEach,
getBaseFileName,
getDirectoryPath,
getNormalizedAbsolutePath,
getOwnKeys,
getPathComponents,
getProperty,
hasJSFileExtension,
mapDefined,
MapLike,
normalizePath,
Path,
readConfigFile,
removeFileExtension,
removeMinAndVersionNumbers,
some,
toFileNameLowerCase,
TypeAcquisition,
Version,
versionMajorMinor,
} from "./_namespaces/ts";
/** @internal */
export interface TypingResolutionHost {
directoryExists(path: string): boolean;
fileExists(fileName: string): boolean;
readFile(path: string, encoding?: string): string | undefined;
readDirectory(rootDir: string, extensions: readonly string[], excludes: readonly string[] | undefined, includes: readonly string[] | undefined, depth?: number): string[];
}
interface PackageJson {
dependencies?: MapLike<string>;
devDependencies?: MapLike<string>;
name?: string;
optionalDependencies?: MapLike<string>;
peerDependencies?: MapLike<string>;
types?: string;
typings?: string;
}
/** @internal */
export interface CachedTyping {
typingLocation: string;
version: Version;
}
/** @internal */
export function isTypingUpToDate(cachedTyping: CachedTyping, availableTypingVersions: MapLike<string>) {
const availableVersion = new Version(getProperty(availableTypingVersions, `ts${versionMajorMinor}`) || getProperty(availableTypingVersions, "latest")!);
return availableVersion.compareTo(cachedTyping.version) <= 0;
}
const unprefixedNodeCoreModuleList = [
"assert",
"assert/strict",
"async_hooks",
"buffer",
"child_process",
"cluster",
"console",
"constants",
"crypto",
"dgram",
"diagnostics_channel",
"dns",
"dns/promises",
"domain",
"events",
"fs",
"fs/promises",
"http",
"https",
"http2",
"inspector",
"module",
"net",
"os",
"path",
"perf_hooks",
"process",
"punycode",
"querystring",
"readline",
"repl",
"stream",
"stream/promises",
"string_decoder",
"timers",
"timers/promises",
"tls",
"trace_events",
"tty",
"url",
"util",
"util/types",
"v8",
"vm",
"wasi",
"worker_threads",
"zlib"
];
/** @internal */
export const prefixedNodeCoreModuleList = unprefixedNodeCoreModuleList.map(name => `node:${name}`);
/** @internal */
export const nodeCoreModuleList: readonly string[] = [...unprefixedNodeCoreModuleList, ...prefixedNodeCoreModuleList];
/** @internal */
export const nodeCoreModules = new Set(nodeCoreModuleList);
/** @internal */
export function nonRelativeModuleNameForTypingCache(moduleName: string) {
return nodeCoreModules.has(moduleName) ? "node" : moduleName;
}
/**
* A map of loose file names to library names that we are confident require typings
*
* @internal
*/
export type SafeList = ReadonlyMap<string, string>;
/** @internal */
export function loadSafeList(host: TypingResolutionHost, safeListPath: Path): SafeList {
const result = readConfigFile(safeListPath, path => host.readFile(path));
return new Map(Object.entries<string>(result.config));
}
/** @internal */
export function loadTypesMap(host: TypingResolutionHost, typesMapPath: Path): SafeList | undefined {
const result = readConfigFile(typesMapPath, path => host.readFile(path));
if (result.config?.simpleMap) {
return new Map(Object.entries<string>(result.config.simpleMap));
}
return undefined;
}
/**
* @param host is the object providing I/O related operations.
* @param fileNames are the file names that belong to the same project
* @param projectRootPath is the path to the project root directory
* @param safeListPath is the path used to retrieve the safe list
* @param packageNameToTypingLocation is the map of package names to their cached typing locations and installed versions
* @param typeAcquisition is used to customize the typing acquisition process
* @param compilerOptions are used as a source for typing inference
*
* @internal
*/
export function discoverTypings(
host: TypingResolutionHost,
log: ((message: string) => void) | undefined,
fileNames: string[],
projectRootPath: Path,
safeList: SafeList,
packageNameToTypingLocation: ReadonlyMap<string, CachedTyping>,
typeAcquisition: TypeAcquisition,
unresolvedImports: readonly string[],
typesRegistry: ReadonlyMap<string, MapLike<string>>,
compilerOptions: CompilerOptions):
{ cachedTypingPaths: string[], newTypingNames: string[], filesToWatch: string[] } {
if (!typeAcquisition || !typeAcquisition.enable) {
return { cachedTypingPaths: [], newTypingNames: [], filesToWatch: [] };
}
// A typing name to typing file path mapping
const inferredTypings = new Map<string, string>();
// Only infer typings for .js and .jsx files
fileNames = mapDefined(fileNames, fileName => {
const path = normalizePath(fileName);
if (hasJSFileExtension(path)) {
return path;
}
});
const filesToWatch: string[] = [];
if (typeAcquisition.include) addInferredTypings(typeAcquisition.include, "Explicitly included types");
const exclude = typeAcquisition.exclude || [];
// Directories to search for package.json, bower.json and other typing information
if (!compilerOptions.types) {
const possibleSearchDirs = new Set(fileNames.map(getDirectoryPath));
possibleSearchDirs.add(projectRootPath);
possibleSearchDirs.forEach((searchDir) => {
getTypingNames(searchDir, "bower.json", "bower_components", filesToWatch);
getTypingNames(searchDir, "package.json", "node_modules", filesToWatch);
});
}
if(!typeAcquisition.disableFilenameBasedTypeAcquisition) {
getTypingNamesFromSourceFileNames(fileNames);
}
// add typings for unresolved imports
if (unresolvedImports) {
const module = deduplicate<string>(
unresolvedImports.map(nonRelativeModuleNameForTypingCache),
equateStringsCaseSensitive,
compareStringsCaseSensitive);
addInferredTypings(module, "Inferred typings from unresolved imports");
}
// Add the cached typing locations for inferred typings that are already installed
packageNameToTypingLocation.forEach((typing, name) => {
const registryEntry = typesRegistry.get(name);
if (inferredTypings.has(name) && inferredTypings.get(name) === undefined && registryEntry !== undefined && isTypingUpToDate(typing, registryEntry)) {
inferredTypings.set(name, typing.typingLocation);
}
});
// Remove typings that the user has added to the exclude list
for (const excludeTypingName of exclude) {
const didDelete = inferredTypings.delete(excludeTypingName);
if (didDelete && log) log(`Typing for ${excludeTypingName} is in exclude list, will be ignored.`);
}
const newTypingNames: string[] = [];
const cachedTypingPaths: string[] = [];
inferredTypings.forEach((inferred, typing) => {
if (inferred !== undefined) {
cachedTypingPaths.push(inferred);
}
else {
newTypingNames.push(typing);
}
});
const result = { cachedTypingPaths, newTypingNames, filesToWatch };
if (log) log(`Result: ${JSON.stringify(result)}`);
return result;
function addInferredTyping(typingName: string) {
if (!inferredTypings.has(typingName)) {
inferredTypings.set(typingName, undefined!); // TODO: GH#18217
}
}
function addInferredTypings(typingNames: readonly string[], message: string) {
if (log) log(`${message}: ${JSON.stringify(typingNames)}`);
forEach(typingNames, addInferredTyping);
}
/**
* Adds inferred typings from manifest/module pairs (think package.json + node_modules)
*
* @param projectRootPath is the path to the directory where to look for package.json, bower.json and other typing information
* @param manifestName is the name of the manifest (package.json or bower.json)
* @param modulesDirName is the directory name for modules (node_modules or bower_components). Should be lowercase!
* @param filesToWatch are the files to watch for changes. We will push things into this array.
*/
function getTypingNames(projectRootPath: string, manifestName: string, modulesDirName: string, filesToWatch: string[]): void {
// First, we check the manifests themselves. They're not
// _required_, but they allow us to do some filtering when dealing
// with big flat dep directories.
const manifestPath = combinePaths(projectRootPath, manifestName);
let manifest;
let manifestTypingNames;
if (host.fileExists(manifestPath)) {
filesToWatch.push(manifestPath);
manifest = readConfigFile(manifestPath, path => host.readFile(path)).config;
manifestTypingNames = flatMap([manifest.dependencies, manifest.devDependencies, manifest.optionalDependencies, manifest.peerDependencies], getOwnKeys);
addInferredTypings(manifestTypingNames, `Typing names in '${manifestPath}' dependencies`);
}
// Now we scan the directories for typing information in
// already-installed dependencies (if present). Note that this
// step happens regardless of whether a manifest was present,
// which is certainly a valid configuration, if an unusual one.
const packagesFolderPath = combinePaths(projectRootPath, modulesDirName);
filesToWatch.push(packagesFolderPath);
if (!host.directoryExists(packagesFolderPath)) {
return;
}
// There's two cases we have to take into account here:
// 1. If manifest is undefined, then we're not using a manifest.
// That means that we should scan _all_ dependencies at the top
// level of the modulesDir.
// 2. If manifest is defined, then we can do some special
// filtering to reduce the amount of scanning we need to do.
//
// Previous versions of this algorithm checked for a `_requiredBy`
// field in the package.json, but that field is only present in
// `npm@>=3 <7`.
// Package names that do **not** provide their own typings, so
// we'll look them up.
const packageNames: string[] = [];
const dependencyManifestNames = manifestTypingNames
// This is #1 described above.
? manifestTypingNames.map(typingName => combinePaths(packagesFolderPath, typingName, manifestName))
// And #2. Depth = 3 because scoped packages look like `node_modules/@foo/bar/package.json`
: host.readDirectory(packagesFolderPath, [Extension.Json], /*excludes*/ undefined, /*includes*/ undefined, /*depth*/ 3)
.filter(manifestPath => {
if (getBaseFileName(manifestPath) !== manifestName) {
return false;
}
// It's ok to treat
// `node_modules/@foo/bar/package.json` as a manifest,
// but not `node_modules/jquery/nested/package.json`.
// We only assume depth 3 is ok for formally scoped
// packages. So that needs this dance here.
const pathComponents = getPathComponents(normalizePath(manifestPath));
const isScoped = pathComponents[pathComponents.length - 3][0] === "@";
return isScoped && toFileNameLowerCase(pathComponents[pathComponents.length - 4]) === modulesDirName || // `node_modules/@foo/bar`
!isScoped && toFileNameLowerCase(pathComponents[pathComponents.length - 3]) === modulesDirName; // `node_modules/foo`
});
if (log) log(`Searching for typing names in ${packagesFolderPath}; all files: ${JSON.stringify(dependencyManifestNames)}`);
// Once we have the names of things to look up, we iterate over
// and either collect their included typings, or add them to the
// list of typings we need to look up separately.
for (const manifestPath of dependencyManifestNames) {
const normalizedFileName = normalizePath(manifestPath);
const result = readConfigFile(normalizedFileName, (path: string) => host.readFile(path));
const manifest: PackageJson = result.config;
// If the package has its own d.ts typings, those will take precedence. Otherwise the package name will be used
// to download d.ts files from DefinitelyTyped
if (!manifest.name) {
continue;
}
const ownTypes = manifest.types || manifest.typings;
if (ownTypes) {
const absolutePath = getNormalizedAbsolutePath(ownTypes, getDirectoryPath(normalizedFileName));
if (host.fileExists(absolutePath)) {
if (log) log(` Package '${manifest.name}' provides its own types.`);
inferredTypings.set(manifest.name, absolutePath);
}
else {
if (log) log(` Package '${manifest.name}' provides its own types but they are missing.`);
}
}
else {
packageNames.push(manifest.name);
}
}
addInferredTypings(packageNames, " Found package names");
}
/**
* Infer typing names from given file names. For example, the file name "jquery-min.2.3.4.js"
* should be inferred to the 'jquery' typing name; and "angular-route.1.2.3.js" should be inferred
* to the 'angular-route' typing name.
* @param fileNames are the names for source files in the project
*/
function getTypingNamesFromSourceFileNames(fileNames: string[]) {
const fromFileNames = mapDefined(fileNames, j => {
if (!hasJSFileExtension(j)) return undefined;
const inferredTypingName = removeFileExtension(toFileNameLowerCase(getBaseFileName(j)));
const cleanedTypingName = removeMinAndVersionNumbers(inferredTypingName);
return safeList.get(cleanedTypingName);
});
if (fromFileNames.length) {
addInferredTypings(fromFileNames, "Inferred typings from file names");
}
const hasJsxFile = some(fileNames, f => fileExtensionIs(f, Extension.Jsx));
if (hasJsxFile) {
if (log) log(`Inferred 'react' typings due to presence of '.jsx' extension`);
addInferredTyping("react");
}
}
}
/** @internal */
export const enum NameValidationResult {
Ok,
EmptyName,
NameTooLong,
NameStartsWithDot,
NameStartsWithUnderscore,
NameContainsNonURISafeCharacters
}
const maxPackageNameLength = 214;
/** @internal */
export interface ScopedPackageNameValidationResult {
name: string;
isScopeName: boolean;
result: NameValidationResult;
}
/** @internal */
export type PackageNameValidationResult = NameValidationResult | ScopedPackageNameValidationResult;
/**
* Validates package name using rules defined at https://docs.npmjs.com/files/package.json
*
* @internal
*/
export function validatePackageName(packageName: string): PackageNameValidationResult {
return validatePackageNameWorker(packageName, /*supportScopedPackage*/ true);
}
function validatePackageNameWorker(packageName: string, supportScopedPackage: false): NameValidationResult;
function validatePackageNameWorker(packageName: string, supportScopedPackage: true): PackageNameValidationResult;
function validatePackageNameWorker(packageName: string, supportScopedPackage: boolean): PackageNameValidationResult {
if (!packageName) {
return NameValidationResult.EmptyName;
}
if (packageName.length > maxPackageNameLength) {
return NameValidationResult.NameTooLong;
}
if (packageName.charCodeAt(0) === CharacterCodes.dot) {
return NameValidationResult.NameStartsWithDot;
}
if (packageName.charCodeAt(0) === CharacterCodes._) {
return NameValidationResult.NameStartsWithUnderscore;
}
// check if name is scope package like: starts with @ and has one '/' in the middle
// scoped packages are not currently supported
if (supportScopedPackage) {
const matches = /^@([^/]+)\/([^/]+)$/.exec(packageName);
if (matches) {
const scopeResult = validatePackageNameWorker(matches[1], /*supportScopedPackage*/ false);
if (scopeResult !== NameValidationResult.Ok) {
return { name: matches[1], isScopeName: true, result: scopeResult };
}
const packageResult = validatePackageNameWorker(matches[2], /*supportScopedPackage*/ false);
if (packageResult !== NameValidationResult.Ok) {
return { name: matches[2], isScopeName: false, result: packageResult };
}
return NameValidationResult.Ok;
}
}
if (encodeURIComponent(packageName) !== packageName) {
return NameValidationResult.NameContainsNonURISafeCharacters;
}
return NameValidationResult.Ok;
}
/** @internal */
export function renderPackageNameValidationFailure(result: PackageNameValidationResult, typing: string): string {
return typeof result === "object" ?
renderPackageNameValidationFailureWorker(typing, result.result, result.name, result.isScopeName) :
renderPackageNameValidationFailureWorker(typing, result, typing, /*isScopeName*/ false);
}
function renderPackageNameValidationFailureWorker(typing: string, result: NameValidationResult, name: string, isScopeName: boolean): string {
const kind = isScopeName ? "Scope" : "Package";
switch (result) {
case NameValidationResult.EmptyName:
return `'${typing}':: ${kind} name '${name}' cannot be empty`;
case NameValidationResult.NameTooLong:
return `'${typing}':: ${kind} name '${name}' should be less than ${maxPackageNameLength} characters`;
case NameValidationResult.NameStartsWithDot:
return `'${typing}':: ${kind} name '${name}' cannot start with '.'`;
case NameValidationResult.NameStartsWithUnderscore:
return `'${typing}':: ${kind} name '${name}' cannot start with '_'`;
case NameValidationResult.NameContainsNonURISafeCharacters:
return `'${typing}':: ${kind} name '${name}' contains non URI safe characters`;
case NameValidationResult.Ok:
return Debug.fail(); // Shouldn't have called this.
default:
throw Debug.assertNever(result);
}
}