• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.JsTyping {
3
4    export interface TypingResolutionHost {
5        directoryExists(path: string): boolean;
6        fileExists(fileName: string): boolean;
7        readFile(path: string, encoding?: string): string | undefined;
8        readDirectory(rootDir: string, extensions: readonly string[], excludes: readonly string[] | undefined, includes: readonly string[] | undefined, depth?: number): string[];
9    }
10
11    interface PackageJson {
12        _requiredBy?: string[];
13        dependencies?: MapLike<string>;
14        devDependencies?: MapLike<string>;
15        name?: string;
16        optionalDependencies?: MapLike<string>;
17        peerDependencies?: MapLike<string>;
18        types?: string;
19        typings?: string;
20    }
21
22    export interface CachedTyping {
23        typingLocation: string;
24        version: Version;
25    }
26
27    export function isTypingUpToDate(cachedTyping: CachedTyping, availableTypingVersions: MapLike<string>) {
28        const availableVersion = new Version(getProperty(availableTypingVersions, `ts${versionMajorMinor}`) || getProperty(availableTypingVersions, "latest")!);
29        return availableVersion.compareTo(cachedTyping.version) <= 0;
30    }
31
32    export const nodeCoreModuleList: readonly string[] = [
33        "assert",
34        "async_hooks",
35        "buffer",
36        "child_process",
37        "cluster",
38        "console",
39        "constants",
40        "crypto",
41        "dgram",
42        "dns",
43        "domain",
44        "events",
45        "fs",
46        "http",
47        "https",
48        "http2",
49        "inspector",
50        "net",
51        "os",
52        "path",
53        "perf_hooks",
54        "process",
55        "punycode",
56        "querystring",
57        "readline",
58        "repl",
59        "stream",
60        "string_decoder",
61        "timers",
62        "tls",
63        "tty",
64        "url",
65        "util",
66        "v8",
67        "vm",
68        "zlib"
69    ];
70
71    export const nodeCoreModules = new Set(nodeCoreModuleList);
72
73    export function nonRelativeModuleNameForTypingCache(moduleName: string) {
74        return nodeCoreModules.has(moduleName) ? "node" : moduleName;
75    }
76
77    /**
78     * A map of loose file names to library names that we are confident require typings
79     */
80    export type SafeList = ReadonlyESMap<string, string>;
81
82    export function loadSafeList(host: TypingResolutionHost, safeListPath: Path): SafeList {
83        const result = readConfigFile(safeListPath, path => host.readFile(path));
84        return new Map(getEntries<string>(result.config));
85    }
86
87    export function loadTypesMap(host: TypingResolutionHost, typesMapPath: Path): SafeList | undefined {
88        const result = readConfigFile(typesMapPath, path => host.readFile(path));
89        if (result.config) {
90            return new Map(getEntries<string>(result.config.simpleMap));
91        }
92        return undefined;
93    }
94
95    /**
96     * @param host is the object providing I/O related operations.
97     * @param fileNames are the file names that belong to the same project
98     * @param projectRootPath is the path to the project root directory
99     * @param safeListPath is the path used to retrieve the safe list
100     * @param packageNameToTypingLocation is the map of package names to their cached typing locations and installed versions
101     * @param typeAcquisition is used to customize the typing acquisition process
102     * @param compilerOptions are used as a source for typing inference
103     */
104    export function discoverTypings(
105        host: TypingResolutionHost,
106        log: ((message: string) => void) | undefined,
107        fileNames: string[],
108        projectRootPath: Path,
109        safeList: SafeList,
110        packageNameToTypingLocation: ReadonlyESMap<string, CachedTyping>,
111        typeAcquisition: TypeAcquisition,
112        unresolvedImports: readonly string[],
113        typesRegistry: ReadonlyESMap<string, MapLike<string>>):
114        { cachedTypingPaths: string[], newTypingNames: string[], filesToWatch: string[] } {
115
116        if (!typeAcquisition || !typeAcquisition.enable) {
117            return { cachedTypingPaths: [], newTypingNames: [], filesToWatch: [] };
118        }
119
120        // A typing name to typing file path mapping
121        const inferredTypings = new Map<string, string>();
122
123        // Only infer typings for .js and .jsx files
124        fileNames = mapDefined(fileNames, fileName => {
125            const path = normalizePath(fileName);
126            if (hasJSFileExtension(path)) {
127                return path;
128            }
129        });
130
131        const filesToWatch: string[] = [];
132
133        if (typeAcquisition.include) addInferredTypings(typeAcquisition.include, "Explicitly included types");
134        const exclude = typeAcquisition.exclude || [];
135
136        // Directories to search for package.json, bower.json and other typing information
137        const possibleSearchDirs = new Set(fileNames.map(getDirectoryPath));
138        possibleSearchDirs.add(projectRootPath);
139        possibleSearchDirs.forEach((searchDir) => {
140            const packageJsonPath = combinePaths(searchDir, "package.json");
141            getTypingNamesFromJson(packageJsonPath, filesToWatch);
142
143            const bowerJsonPath = combinePaths(searchDir, "bower.json");
144            getTypingNamesFromJson(bowerJsonPath, filesToWatch);
145
146            const bowerComponentsPath = combinePaths(searchDir, "bower_components");
147            getTypingNamesFromPackagesFolder(bowerComponentsPath, filesToWatch);
148
149            const nodeModulesPath = combinePaths(searchDir, "node_modules");
150            getTypingNamesFromPackagesFolder(nodeModulesPath, filesToWatch);
151        });
152        if(!typeAcquisition.disableFilenameBasedTypeAcquisition) {
153            getTypingNamesFromSourceFileNames(fileNames);
154        }
155        // add typings for unresolved imports
156        if (unresolvedImports) {
157            const module = deduplicate<string>(
158                unresolvedImports.map(nonRelativeModuleNameForTypingCache),
159                equateStringsCaseSensitive,
160                compareStringsCaseSensitive);
161            addInferredTypings(module, "Inferred typings from unresolved imports");
162        }
163        // Add the cached typing locations for inferred typings that are already installed
164        packageNameToTypingLocation.forEach((typing, name) => {
165            const registryEntry = typesRegistry.get(name);
166            if (inferredTypings.has(name) && inferredTypings.get(name) === undefined && registryEntry !== undefined && isTypingUpToDate(typing, registryEntry)) {
167                inferredTypings.set(name, typing.typingLocation);
168            }
169        });
170
171        // Remove typings that the user has added to the exclude list
172        for (const excludeTypingName of exclude) {
173            const didDelete = inferredTypings.delete(excludeTypingName);
174            if (didDelete && log) log(`Typing for ${excludeTypingName} is in exclude list, will be ignored.`);
175        }
176
177        const newTypingNames: string[] = [];
178        const cachedTypingPaths: string[] = [];
179        inferredTypings.forEach((inferred, typing) => {
180            if (inferred !== undefined) {
181                cachedTypingPaths.push(inferred);
182            }
183            else {
184                newTypingNames.push(typing);
185            }
186        });
187        const result = { cachedTypingPaths, newTypingNames, filesToWatch };
188        if (log) log(`Result: ${JSON.stringify(result)}`);
189        return result;
190
191        function addInferredTyping(typingName: string) {
192            if (!inferredTypings.has(typingName)) {
193                inferredTypings.set(typingName, undefined!); // TODO: GH#18217
194            }
195        }
196        function addInferredTypings(typingNames: readonly string[], message: string) {
197            if (log) log(`${message}: ${JSON.stringify(typingNames)}`);
198            forEach(typingNames, addInferredTyping);
199        }
200
201        /**
202         * Get the typing info from common package manager json files like package.json or bower.json
203         */
204        function getTypingNamesFromJson(jsonPath: string, filesToWatch: Push<string>) {
205            if (!host.fileExists(jsonPath)) {
206                return;
207            }
208
209            filesToWatch.push(jsonPath);
210            const jsonConfig: PackageJson = readConfigFile(jsonPath, path => host.readFile(path)).config;
211            const jsonTypingNames = flatMap([jsonConfig.dependencies, jsonConfig.devDependencies, jsonConfig.optionalDependencies, jsonConfig.peerDependencies], getOwnKeys);
212            addInferredTypings(jsonTypingNames, `Typing names in '${jsonPath}' dependencies`);
213        }
214
215        /**
216         * Infer typing names from given file names. For example, the file name "jquery-min.2.3.4.js"
217         * should be inferred to the 'jquery' typing name; and "angular-route.1.2.3.js" should be inferred
218         * to the 'angular-route' typing name.
219         * @param fileNames are the names for source files in the project
220         */
221        function getTypingNamesFromSourceFileNames(fileNames: string[]) {
222            const fromFileNames = mapDefined(fileNames, j => {
223                if (!hasJSFileExtension(j)) return undefined;
224
225                const inferredTypingName = removeFileExtension(getBaseFileName(j.toLowerCase()));
226                const cleanedTypingName = removeMinAndVersionNumbers(inferredTypingName);
227                return safeList.get(cleanedTypingName);
228            });
229            if (fromFileNames.length) {
230                addInferredTypings(fromFileNames, "Inferred typings from file names");
231            }
232
233            const hasJsxFile = some(fileNames, f => fileExtensionIs(f, Extension.Jsx));
234            if (hasJsxFile) {
235                if (log) log(`Inferred 'react' typings due to presence of '.jsx' extension`);
236                addInferredTyping("react");
237            }
238        }
239
240        /**
241         * Infer typing names from packages folder (ex: node_module, bower_components)
242         * @param packagesFolderPath is the path to the packages folder
243         */
244        function getTypingNamesFromPackagesFolder(packagesFolderPath: string, filesToWatch: Push<string>) {
245            filesToWatch.push(packagesFolderPath);
246
247            // Todo: add support for ModuleResolutionHost too
248            if (!host.directoryExists(packagesFolderPath)) {
249                return;
250            }
251
252            // depth of 2, so we access `node_modules/foo` but not `node_modules/foo/bar`
253            const fileNames = host.readDirectory(packagesFolderPath, [Extension.Json], /*excludes*/ undefined, /*includes*/ undefined, /*depth*/ 2);
254            if (log) log(`Searching for typing names in ${packagesFolderPath}; all files: ${JSON.stringify(fileNames)}`);
255            const packageNames: string[] = [];
256            for (const fileName of fileNames) {
257                const normalizedFileName = normalizePath(fileName);
258                const baseFileName = getBaseFileName(normalizedFileName);
259                if (baseFileName !== "package.json" && baseFileName !== "bower.json") {
260                    continue;
261                }
262                const result = readConfigFile(normalizedFileName, (path: string) => host.readFile(path));
263                const packageJson: PackageJson = result.config;
264
265                // npm 3's package.json contains a "_requiredBy" field
266                // we should include all the top level module names for npm 2, and only module names whose
267                // "_requiredBy" field starts with "#" or equals "/" for npm 3.
268                if (baseFileName === "package.json" && packageJson._requiredBy &&
269                    filter(packageJson._requiredBy, (r: string) => r[0] === "#" || r === "/").length === 0) {
270                    continue;
271                }
272
273                // If the package has its own d.ts typings, those will take precedence. Otherwise the package name will be used
274                // to download d.ts files from DefinitelyTyped
275                if (!packageJson.name) {
276                    continue;
277                }
278                const ownTypes = packageJson.types || packageJson.typings;
279                if (ownTypes) {
280                    const absolutePath = getNormalizedAbsolutePath(ownTypes, getDirectoryPath(normalizedFileName));
281                    if (log) log(`    Package '${packageJson.name}' provides its own types.`);
282                    inferredTypings.set(packageJson.name, absolutePath);
283                }
284                else {
285                    packageNames.push(packageJson.name);
286                }
287            }
288            addInferredTypings(packageNames, "    Found package names");
289        }
290
291    }
292
293    export const enum NameValidationResult {
294        Ok,
295        EmptyName,
296        NameTooLong,
297        NameStartsWithDot,
298        NameStartsWithUnderscore,
299        NameContainsNonURISafeCharacters
300    }
301
302    const maxPackageNameLength = 214;
303
304    export interface ScopedPackageNameValidationResult {
305        name: string;
306        isScopeName: boolean;
307        result: NameValidationResult;
308    }
309    export type PackageNameValidationResult = NameValidationResult | ScopedPackageNameValidationResult;
310
311    /**
312     * Validates package name using rules defined at https://docs.npmjs.com/files/package.json
313     */
314    export function validatePackageName(packageName: string): PackageNameValidationResult {
315        return validatePackageNameWorker(packageName, /*supportScopedPackage*/ true);
316    }
317
318    function validatePackageNameWorker(packageName: string, supportScopedPackage: false): NameValidationResult;
319    function validatePackageNameWorker(packageName: string, supportScopedPackage: true): PackageNameValidationResult;
320    function validatePackageNameWorker(packageName: string, supportScopedPackage: boolean): PackageNameValidationResult {
321        if (!packageName) {
322            return NameValidationResult.EmptyName;
323        }
324        if (packageName.length > maxPackageNameLength) {
325            return NameValidationResult.NameTooLong;
326        }
327        if (packageName.charCodeAt(0) === CharacterCodes.dot) {
328            return NameValidationResult.NameStartsWithDot;
329        }
330        if (packageName.charCodeAt(0) === CharacterCodes._) {
331            return NameValidationResult.NameStartsWithUnderscore;
332        }
333        // check if name is scope package like: starts with @ and has one '/' in the middle
334        // scoped packages are not currently supported
335        if (supportScopedPackage) {
336            const matches = /^@([^/]+)\/([^/]+)$/.exec(packageName);
337            if (matches) {
338                const scopeResult = validatePackageNameWorker(matches[1], /*supportScopedPackage*/ false);
339                if (scopeResult !== NameValidationResult.Ok) {
340                    return { name: matches[1], isScopeName: true, result: scopeResult };
341                }
342                const packageResult = validatePackageNameWorker(matches[2], /*supportScopedPackage*/ false);
343                if (packageResult !== NameValidationResult.Ok) {
344                    return { name: matches[2], isScopeName: false, result: packageResult };
345                }
346                return NameValidationResult.Ok;
347            }
348        }
349        if (encodeURIComponent(packageName) !== packageName) {
350            return NameValidationResult.NameContainsNonURISafeCharacters;
351        }
352        return NameValidationResult.Ok;
353    }
354
355    export function renderPackageNameValidationFailure(result: PackageNameValidationResult, typing: string): string {
356        return typeof result === "object" ?
357            renderPackageNameValidationFailureWorker(typing, result.result, result.name, result.isScopeName) :
358            renderPackageNameValidationFailureWorker(typing, result, typing, /*isScopeName*/ false);
359    }
360
361    function renderPackageNameValidationFailureWorker(typing: string, result: NameValidationResult, name: string, isScopeName: boolean): string {
362        const kind = isScopeName ? "Scope" : "Package";
363        switch (result) {
364            case NameValidationResult.EmptyName:
365                return `'${typing}':: ${kind} name '${name}' cannot be empty`;
366            case NameValidationResult.NameTooLong:
367                return `'${typing}':: ${kind} name '${name}' should be less than ${maxPackageNameLength} characters`;
368            case NameValidationResult.NameStartsWithDot:
369                return `'${typing}':: ${kind} name '${name}' cannot start with '.'`;
370            case NameValidationResult.NameStartsWithUnderscore:
371                return `'${typing}':: ${kind} name '${name}' cannot start with '_'`;
372            case NameValidationResult.NameContainsNonURISafeCharacters:
373                return `'${typing}':: ${kind} name '${name}' contains non URI safe characters`;
374            case NameValidationResult.Ok:
375                return Debug.fail(); // Shouldn't have called this.
376            default:
377                throw Debug.assertNever(result);
378        }
379    }
380}
381