mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-05 08:11:30 -06:00
fix ATA toplevel dep detection (#47164)
ATA tried to use the `_requiredBy` field to determine toplevel deps, but this is not portable. Not only is it unavailable in npm@>=7, but neither Yarn nor pnpm write this metadata to node_modules pkgjsons. This also adds support for ATA acquiring types for scoped packages. Fixes: https://github.com/microsoft/TypeScript/issues/44130
This commit is contained in:
parent
8a8c71c147
commit
7f189b9701
@ -9,7 +9,6 @@ namespace ts.JsTyping {
|
||||
}
|
||||
|
||||
interface PackageJson {
|
||||
_requiredBy?: string[];
|
||||
dependencies?: MapLike<string>;
|
||||
devDependencies?: MapLike<string>;
|
||||
name?: string;
|
||||
@ -152,17 +151,8 @@ namespace ts.JsTyping {
|
||||
const possibleSearchDirs = new Set(fileNames.map(getDirectoryPath));
|
||||
possibleSearchDirs.add(projectRootPath);
|
||||
possibleSearchDirs.forEach((searchDir) => {
|
||||
const packageJsonPath = combinePaths(searchDir, "package.json");
|
||||
getTypingNamesFromJson(packageJsonPath, filesToWatch);
|
||||
|
||||
const bowerJsonPath = combinePaths(searchDir, "bower.json");
|
||||
getTypingNamesFromJson(bowerJsonPath, filesToWatch);
|
||||
|
||||
const bowerComponentsPath = combinePaths(searchDir, "bower_components");
|
||||
getTypingNamesFromPackagesFolder(bowerComponentsPath, filesToWatch);
|
||||
|
||||
const nodeModulesPath = combinePaths(searchDir, "node_modules");
|
||||
getTypingNamesFromPackagesFolder(nodeModulesPath, filesToWatch);
|
||||
getTypingNames(searchDir, "bower.json", "bower_components", filesToWatch);
|
||||
getTypingNames(searchDir, "package.json", "node_modules", filesToWatch);
|
||||
});
|
||||
if(!typeAcquisition.disableFilenameBasedTypeAcquisition) {
|
||||
getTypingNamesFromSourceFileNames(fileNames);
|
||||
@ -214,17 +204,104 @@ namespace ts.JsTyping {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the typing info from common package manager json files like package.json or bower.json
|
||||
* 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 getTypingNamesFromJson(jsonPath: string, filesToWatch: Push<string>) {
|
||||
if (!host.fileExists(jsonPath)) {
|
||||
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;
|
||||
}
|
||||
|
||||
filesToWatch.push(jsonPath);
|
||||
const jsonConfig: PackageJson = readConfigFile(jsonPath, path => host.readFile(path)).config;
|
||||
const jsonTypingNames = flatMap([jsonConfig.dependencies, jsonConfig.devDependencies, jsonConfig.optionalDependencies, jsonConfig.peerDependencies], getOwnKeys);
|
||||
addInferredTypings(jsonTypingNames, `Typing names in '${jsonPath}' dependencies`);
|
||||
// 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 && pathComponents[pathComponents.length - 4].toLowerCase() === modulesDirName || // `node_modules/@foo/bar`
|
||||
!isScoped && pathComponents[pathComponents.length - 3].toLowerCase() === 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");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -251,58 +328,6 @@ namespace ts.JsTyping {
|
||||
addInferredTyping("react");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer typing names from packages folder (ex: node_module, bower_components)
|
||||
* @param packagesFolderPath is the path to the packages folder
|
||||
*/
|
||||
function getTypingNamesFromPackagesFolder(packagesFolderPath: string, filesToWatch: Push<string>) {
|
||||
filesToWatch.push(packagesFolderPath);
|
||||
|
||||
// Todo: add support for ModuleResolutionHost too
|
||||
if (!host.directoryExists(packagesFolderPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// depth of 2, so we access `node_modules/foo` but not `node_modules/foo/bar`
|
||||
const fileNames = host.readDirectory(packagesFolderPath, [Extension.Json], /*excludes*/ undefined, /*includes*/ undefined, /*depth*/ 2);
|
||||
if (log) log(`Searching for typing names in ${packagesFolderPath}; all files: ${JSON.stringify(fileNames)}`);
|
||||
const packageNames: string[] = [];
|
||||
for (const fileName of fileNames) {
|
||||
const normalizedFileName = normalizePath(fileName);
|
||||
const baseFileName = getBaseFileName(normalizedFileName);
|
||||
if (baseFileName !== "package.json" && baseFileName !== "bower.json") {
|
||||
continue;
|
||||
}
|
||||
const result = readConfigFile(normalizedFileName, (path: string) => host.readFile(path));
|
||||
const packageJson: PackageJson = result.config;
|
||||
|
||||
// npm 3's package.json contains a "_requiredBy" field
|
||||
// we should include all the top level module names for npm 2, and only module names whose
|
||||
// "_requiredBy" field starts with "#" or equals "/" for npm 3.
|
||||
if (baseFileName === "package.json" && packageJson._requiredBy &&
|
||||
filter(packageJson._requiredBy, (r: string) => r[0] === "#" || r === "/").length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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 (!packageJson.name) {
|
||||
continue;
|
||||
}
|
||||
const ownTypes = packageJson.types || packageJson.typings;
|
||||
if (ownTypes) {
|
||||
const absolutePath = getNormalizedAbsolutePath(ownTypes, getDirectoryPath(normalizedFileName));
|
||||
if (log) log(` Package '${packageJson.name}' provides its own types.`);
|
||||
inferredTypings.set(packageJson.name, absolutePath);
|
||||
}
|
||||
else {
|
||||
packageNames.push(packageJson.name);
|
||||
}
|
||||
}
|
||||
addInferredTypings(packageNames, " Found package names");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const enum NameValidationResult {
|
||||
|
||||
@ -834,15 +834,101 @@ namespace ts.projectSystem {
|
||||
checkProjectActualFiles(p2, [file3.path, grunt.path, gulp.path]);
|
||||
});
|
||||
|
||||
it("configured scoped name projects discover from node_modules", () => {
|
||||
const app = {
|
||||
path: "/app.js",
|
||||
content: ""
|
||||
};
|
||||
const pkgJson = {
|
||||
path: "/package.json",
|
||||
content: JSON.stringify({
|
||||
dependencies: {
|
||||
"@zkat/cacache": "1.0.0"
|
||||
}
|
||||
})
|
||||
};
|
||||
const jsconfig = {
|
||||
path: "/jsconfig.json",
|
||||
content: JSON.stringify({})
|
||||
};
|
||||
// Should only accept direct dependencies.
|
||||
const commander = {
|
||||
path: "/node_modules/commander/index.js",
|
||||
content: ""
|
||||
};
|
||||
const commanderPackage = {
|
||||
path: "/node_modules/commander/package.json",
|
||||
content: JSON.stringify({
|
||||
name: "commander",
|
||||
})
|
||||
};
|
||||
const cacache = {
|
||||
path: "/node_modules/@zkat/cacache/index.js",
|
||||
content: ""
|
||||
};
|
||||
const cacachePackage = {
|
||||
path: "/node_modules/@zkat/cacache/package.json",
|
||||
content: JSON.stringify({ name: "@zkat/cacache" })
|
||||
};
|
||||
const cacacheDTS = {
|
||||
path: "/tmp/node_modules/@types/zkat__cacache/index.d.ts",
|
||||
content: ""
|
||||
};
|
||||
const host = createServerHost([app, jsconfig, pkgJson, commander, commanderPackage, cacache, cacachePackage]);
|
||||
const installer = new (class extends Installer {
|
||||
constructor() {
|
||||
super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("zkat__cacache", "nested", "commander") });
|
||||
}
|
||||
installWorker(_requestId: number, args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
|
||||
assert.deepEqual(args, [`@types/zkat__cacache@ts${versionMajorMinor}`]);
|
||||
const installedTypings = ["@types/zkat__cacache"];
|
||||
const typingFiles = [cacacheDTS];
|
||||
executeCommand(this, host, installedTypings, typingFiles, cb);
|
||||
}
|
||||
})();
|
||||
|
||||
const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer });
|
||||
projectService.openClientFile(app.path);
|
||||
|
||||
checkNumberOfProjects(projectService, { configuredProjects: 1 });
|
||||
const p = configuredProjectAt(projectService, 0);
|
||||
checkProjectActualFiles(p, [app.path, jsconfig.path]);
|
||||
|
||||
installer.installAll(/*expectedCount*/ 1);
|
||||
|
||||
checkNumberOfProjects(projectService, { configuredProjects: 1 });
|
||||
host.checkTimeoutQueueLengthAndRun(2);
|
||||
checkProjectActualFiles(p, [app.path, cacacheDTS.path, jsconfig.path]);
|
||||
});
|
||||
|
||||
it("configured projects discover from node_modules", () => {
|
||||
const app = {
|
||||
path: "/app.js",
|
||||
content: ""
|
||||
};
|
||||
const pkgJson = {
|
||||
path: "/package.json",
|
||||
content: JSON.stringify({
|
||||
dependencies: {
|
||||
jquery: "1.0.0"
|
||||
}
|
||||
})
|
||||
};
|
||||
const jsconfig = {
|
||||
path: "/jsconfig.json",
|
||||
content: JSON.stringify({})
|
||||
};
|
||||
// Should only accept direct dependencies.
|
||||
const commander = {
|
||||
path: "/node_modules/commander/index.js",
|
||||
content: ""
|
||||
};
|
||||
const commanderPackage = {
|
||||
path: "/node_modules/commander/package.json",
|
||||
content: JSON.stringify({
|
||||
name: "commander",
|
||||
})
|
||||
};
|
||||
const jquery = {
|
||||
path: "/node_modules/jquery/index.js",
|
||||
content: ""
|
||||
@ -860,10 +946,10 @@ namespace ts.projectSystem {
|
||||
path: "/tmp/node_modules/@types/jquery/index.d.ts",
|
||||
content: ""
|
||||
};
|
||||
const host = createServerHost([app, jsconfig, jquery, jqueryPackage, nestedPackage]);
|
||||
const host = createServerHost([app, jsconfig, pkgJson, commander, commanderPackage, jquery, jqueryPackage, nestedPackage]);
|
||||
const installer = new (class extends Installer {
|
||||
constructor() {
|
||||
super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("jquery", "nested") });
|
||||
super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("jquery", "nested", "commander") });
|
||||
}
|
||||
installWorker(_requestId: number, args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
|
||||
assert.deepEqual(args, [`@types/jquery@ts${versionMajorMinor}`]);
|
||||
@ -901,7 +987,7 @@ namespace ts.projectSystem {
|
||||
content: ""
|
||||
};
|
||||
const jqueryPackage = {
|
||||
path: "/bower_components/jquery/package.json",
|
||||
path: "/bower_components/jquery/bower.json",
|
||||
content: JSON.stringify({ name: "jquery" })
|
||||
};
|
||||
const jqueryDTS = {
|
||||
@ -1556,6 +1642,31 @@ namespace ts.projectSystem {
|
||||
});
|
||||
});
|
||||
|
||||
it("should support scoped packages", () => {
|
||||
const app = {
|
||||
path: "/app.js",
|
||||
content: "",
|
||||
};
|
||||
const a = {
|
||||
path: "/node_modules/@a/b/package.json",
|
||||
content: JSON.stringify({ name: "@a/b" }),
|
||||
};
|
||||
const host = createServerHost([app, a]);
|
||||
const cache = new Map<string, JsTyping.CachedTyping>();
|
||||
const logger = trackingLogger();
|
||||
const result = JsTyping.discoverTypings(host, logger.log, [app.path], getDirectoryPath(app.path as Path), emptySafeList, cache, { enable: true }, /*unresolvedImports*/ [], emptyMap);
|
||||
assert.deepEqual(logger.finish(), [
|
||||
'Searching for typing names in /node_modules; all files: ["/node_modules/@a/b/package.json"]',
|
||||
' Found package names: ["@a/b"]',
|
||||
"Inferred typings from unresolved imports: []",
|
||||
'Result: {"cachedTypingPaths":[],"newTypingNames":["@a/b"],"filesToWatch":["/bower_components","/node_modules"]}',
|
||||
]);
|
||||
assert.deepEqual(result, {
|
||||
cachedTypingPaths: [],
|
||||
newTypingNames: ["@a/b"],
|
||||
filesToWatch: ["/bower_components", "/node_modules"],
|
||||
});
|
||||
});
|
||||
it("should install expired typings", () => {
|
||||
const app = {
|
||||
path: "/a/app.js",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user