Skip to content

Commit b774b54

Browse files
committedJul 24, 2024··
feat: add Yarn PnP support
1 parent b4732bd commit b774b54

16 files changed

+465
-46
lines changed
 

‎.github/workflows/ci.yml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches:
66
- main
77
- release-*
8+
- '*/pnp-*'
89
pull_request:
910
branches:
1011
- main

‎src/compiler/moduleNameResolver.ts

+79-2
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ import {
108108
versionMajorMinor,
109109
VersionRange,
110110
} from "./_namespaces/ts.js";
111+
import { getPnpTypeRoots } from "./pnp.js";
112+
import { getPnpApi } from "./pnpapi.js";
111113

112114
/** @internal */
113115
export function trace(host: ModuleResolutionHost, message: DiagnosticMessage, ...args: any[]): void {
@@ -493,7 +495,7 @@ export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffecti
493495
* Returns the path to every node_modules/@types directory from some ancestor directory.
494496
* Returns undefined if there are none.
495497
*/
496-
function getDefaultTypeRoots(currentDirectory: string): string[] | undefined {
498+
function getNodeModulesTypeRoots(currentDirectory: string) {
497499
let typeRoots: string[] | undefined;
498500
forEachAncestorDirectory(normalizePath(currentDirectory), directory => {
499501
const atTypes = combinePaths(directory, nodeModulesAtTypes);
@@ -508,6 +510,18 @@ function arePathsEqual(path1: string, path2: string, host: ModuleResolutionHost)
508510
return comparePaths(path1, path2, !useCaseSensitiveFileNames) === Comparison.EqualTo;
509511
}
510512

513+
function getDefaultTypeRoots(currentDirectory: string): string[] | undefined {
514+
const nmTypes = getNodeModulesTypeRoots(currentDirectory);
515+
const pnpTypes = getPnpTypeRoots(currentDirectory);
516+
517+
if (nmTypes?.length) {
518+
return [...nmTypes, ...pnpTypes];
519+
}
520+
else if (pnpTypes.length) {
521+
return pnpTypes;
522+
}
523+
}
524+
511525
function getOriginalAndResolvedFileName(fileName: string, host: ModuleResolutionHost, traceEnabled: boolean) {
512526
const resolvedFileName = realPath(fileName, host, traceEnabled);
513527
const pathsAreEqual = arePathsEqual(fileName, resolvedFileName, host);
@@ -788,6 +802,18 @@ export function resolvePackageNameToPackageJson(
788802
): PackageJsonInfo | undefined {
789803
const moduleResolutionState = getTemporaryModuleResolutionState(cache?.getPackageJsonInfoCache(), host, options);
790804

805+
const pnpapi = getPnpApi(containingDirectory);
806+
if (pnpapi) {
807+
try {
808+
const resolution = pnpapi.resolveToUnqualified(packageName, `${containingDirectory}/`, { considerBuiltins: false });
809+
const candidate = normalizeSlashes(resolution).replace(/\/$/, "");
810+
return getPackageJsonInfo(candidate, /*onlyRecordFailures*/ false, moduleResolutionState);
811+
}
812+
catch {
813+
return;
814+
}
815+
}
816+
791817
return forEachAncestorDirectory(containingDirectory, ancestorDirectory => {
792818
if (getBaseFileName(ancestorDirectory) !== "node_modules") {
793819
const nodeModulesFolder = combinePaths(ancestorDirectory, "node_modules");
@@ -3002,7 +3028,16 @@ function loadModuleFromNearestNodeModulesDirectoryWorker(extensions: Extensions,
30023028
}
30033029

30043030
function lookup(extensions: Extensions) {
3005-
return forEachAncestorDirectory(normalizeSlashes(directory), ancestorDirectory => {
3031+
const issuer = normalizeSlashes(directory);
3032+
if (getPnpApi(issuer)) {
3033+
const resolutionFromCache = tryFindNonRelativeModuleNameInCache(cache, moduleName, mode, issuer, redirectedReference, state);
3034+
if (resolutionFromCache) {
3035+
return resolutionFromCache;
3036+
}
3037+
return toSearchResult(loadModuleFromImmediateNodeModulesDirectoryPnP(extensions, moduleName, issuer, state, typesScopeOnly, cache, redirectedReference));
3038+
}
3039+
3040+
return forEachAncestorDirectory(issuer, ancestorDirectory => {
30063041
if (getBaseFileName(ancestorDirectory) !== "node_modules") {
30073042
const resolutionFromCache = tryFindNonRelativeModuleNameInCache(cache, moduleName, mode, ancestorDirectory, redirectedReference, state);
30083043
if (resolutionFromCache) {
@@ -3041,11 +3076,34 @@ function loadModuleFromImmediateNodeModulesDirectory(extensions: Extensions, mod
30413076
}
30423077
}
30433078

3079+
function loadModuleFromImmediateNodeModulesDirectoryPnP(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
3080+
const issuer = normalizeSlashes(directory);
3081+
3082+
if (!typesScopeOnly) {
3083+
const packageResult = tryLoadModuleUsingPnpResolution(extensions, moduleName, issuer, state, cache, redirectedReference);
3084+
if (packageResult) {
3085+
return packageResult;
3086+
}
3087+
}
3088+
3089+
if (extensions & Extensions.Declaration) {
3090+
return tryLoadModuleUsingPnpResolution(Extensions.Declaration, `@types/${mangleScopedPackageNameWithTrace(moduleName, state)}`, issuer, state, cache, redirectedReference);
3091+
}
3092+
}
3093+
30443094
function loadModuleFromSpecificNodeModulesDirectory(extensions: Extensions, moduleName: string, nodeModulesDirectory: string, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
30453095
const candidate = normalizePath(combinePaths(nodeModulesDirectory, moduleName));
30463096
const { packageName, rest } = parsePackageName(moduleName);
30473097
const packageDirectory = combinePaths(nodeModulesDirectory, packageName);
3098+
return loadModuleFromSpecificNodeModulesDirectoryImpl(extensions, nodeModulesDirectoryExists, state, cache, redirectedReference, candidate, rest, packageDirectory);
3099+
}
3100+
3101+
function loadModuleFromPnpResolution(extensions: Extensions, packageDirectory: string, rest: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
3102+
const candidate = normalizePath(combinePaths(packageDirectory, rest));
3103+
return loadModuleFromSpecificNodeModulesDirectoryImpl(extensions, /*nodeModulesDirectoryExists*/ true, state, cache, redirectedReference, candidate, rest, packageDirectory);
3104+
}
30483105

3106+
function loadModuleFromSpecificNodeModulesDirectoryImpl(extensions: Extensions, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined, candidate: string, rest: string, packageDirectory: string): Resolved | undefined {
30493107
let rootPackageInfo: PackageJsonInfo | undefined;
30503108
// First look for a nested package.json, as in `node_modules/foo/bar/package.json`.
30513109
let packageInfo = getPackageJsonInfo(candidate, !nodeModulesDirectoryExists, state);
@@ -3377,3 +3435,22 @@ function useCaseSensitiveFileNames(state: ModuleResolutionState) {
33773435
typeof state.host.useCaseSensitiveFileNames === "boolean" ? state.host.useCaseSensitiveFileNames :
33783436
state.host.useCaseSensitiveFileNames();
33793437
}
3438+
3439+
function loadPnpPackageResolution(packageName: string, containingDirectory: string) {
3440+
try {
3441+
const resolution = getPnpApi(containingDirectory).resolveToUnqualified(packageName, `${containingDirectory}/`, { considerBuiltins: false });
3442+
return normalizeSlashes(resolution).replace(/\/$/, "");
3443+
}
3444+
catch {
3445+
// Nothing to do
3446+
}
3447+
}
3448+
3449+
function tryLoadModuleUsingPnpResolution(extensions: Extensions, moduleName: string, containingDirectory: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined) {
3450+
const { packageName, rest } = parsePackageName(moduleName);
3451+
3452+
const packageResolution = loadPnpPackageResolution(packageName, containingDirectory);
3453+
return packageResolution
3454+
? loadModuleFromPnpResolution(extensions, packageResolution, rest, state, cache, redirectedReference)
3455+
: undefined;
3456+
}

‎src/compiler/moduleSpecifiers.ts

+77-14
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ import {
9797
NodeModulePathParts,
9898
normalizePath,
9999
PackageJsonPathFields,
100+
PackagePathParts,
100101
pathContainsNodeModules,
101102
pathIsBareSpecifier,
102103
pathIsRelative,
@@ -126,6 +127,7 @@ import {
126127
TypeChecker,
127128
UserPreferences,
128129
} from "./_namespaces/ts.js";
130+
import { getPnpApi } from "./pnpapi.js";
129131

130132
// Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers.
131133

@@ -762,7 +764,17 @@ function getAllModulePathsWorker(info: Info, importedFileName: string, host: Mod
762764
host,
763765
/*preferSymlinks*/ true,
764766
(path, isRedirect) => {
765-
const isInNodeModules = pathContainsNodeModules(path);
767+
let isInNodeModules = pathContainsNodeModules(path);
768+
769+
const pnpapi = getPnpApi(path);
770+
if (!isInNodeModules && pnpapi) {
771+
const fromLocator = pnpapi.findPackageLocator(info.importingSourceFileName);
772+
const toLocator = pnpapi.findPackageLocator(path);
773+
if (fromLocator && toLocator && fromLocator !== toLocator) {
774+
isInNodeModules = true;
775+
}
776+
}
777+
766778
allFileNames.set(path, { path: info.getCanonicalFileName(path), isRedirect, isInNodeModules });
767779
importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules;
768780
// don't return value, so we collect everything
@@ -1093,7 +1105,51 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
10931105
if (!host.fileExists || !host.readFile) {
10941106
return undefined;
10951107
}
1096-
const parts: NodeModulePathParts = getNodeModulePathParts(path)!;
1108+
let parts: NodeModulePathParts | PackagePathParts | undefined = getNodeModulePathParts(path);
1109+
1110+
let pnpPackageName: string | undefined;
1111+
1112+
const pnpApi = getPnpApi(path);
1113+
if (pnpApi) {
1114+
const fromLocator = pnpApi.findPackageLocator(importingSourceFile.fileName);
1115+
const toLocator = pnpApi.findPackageLocator(path);
1116+
1117+
// Don't use the package name when the imported file is inside
1118+
// the source directory (prefer a relative path instead)
1119+
if (fromLocator === toLocator) {
1120+
return undefined;
1121+
}
1122+
1123+
if (fromLocator && toLocator) {
1124+
const fromInfo = pnpApi.getPackageInformation(fromLocator);
1125+
if (toLocator.reference === fromInfo.packageDependencies.get(toLocator.name)) {
1126+
pnpPackageName = toLocator.name;
1127+
}
1128+
else {
1129+
// Aliased dependencies
1130+
for (const [name, reference] of fromInfo.packageDependencies) {
1131+
if (Array.isArray(reference)) {
1132+
if (reference[0] === toLocator.name && reference[1] === toLocator.reference) {
1133+
pnpPackageName = name;
1134+
break;
1135+
}
1136+
}
1137+
}
1138+
}
1139+
1140+
if (!parts) {
1141+
const toInfo = pnpApi.getPackageInformation(toLocator);
1142+
parts = {
1143+
topLevelNodeModulesIndex: undefined,
1144+
topLevelPackageNameIndex: undefined,
1145+
// The last character from packageLocation is the trailing "/", we want to point to it
1146+
packageRootIndex: toInfo.packageLocation.length - 1,
1147+
fileNameIndex: path.lastIndexOf(`/`),
1148+
};
1149+
}
1150+
}
1151+
}
1152+
10971153
if (!parts) {
10981154
return undefined;
10991155
}
@@ -1138,19 +1194,26 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
11381194
return undefined;
11391195
}
11401196

1141-
const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
1142-
// Get a path that's relative to node_modules or the importing file's path
1143-
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
1144-
const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
1145-
if (!(startsWith(canonicalSourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
1146-
return undefined;
1197+
// If PnP is enabled the node_modules entries we'll get will always be relevant even if they
1198+
// are located in a weird path apparently outside of the source directory
1199+
if (typeof process.versions.pnp === "undefined") {
1200+
const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
1201+
// Get a path that's relative to node_modules or the importing file's path
1202+
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
1203+
const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
1204+
if (!(startsWith(canonicalSourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
1205+
return undefined;
1206+
}
11471207
}
11481208

11491209
// If the module was found in @types, get the actual Node package name
1150-
const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1);
1151-
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
1210+
const nodeModulesDirectoryName = typeof pnpPackageName !== "undefined"
1211+
? pnpPackageName + moduleSpecifier.substring(parts.packageRootIndex)
1212+
: moduleSpecifier.substring(parts.topLevelPackageNameIndex! + 1);
1213+
1214+
const packageNameFromPath = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
11521215
// For classic resolution, only allow importing from node_modules/@types, not other node_modules
1153-
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Classic && packageName === nodeModulesDirectoryName ? undefined : packageName;
1216+
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Classic && packageNameFromPath === nodeModulesDirectoryName ? undefined : packageNameFromPath;
11541217

11551218
function tryDirectoryWithPackageJson(packageRootIndex: number): { moduleFileToTry: string; packageRootPath?: string; blockedByExports?: true; verbatimFromExports?: true; } {
11561219
const packageRootPath = path.substring(0, packageRootIndex);
@@ -1165,8 +1228,8 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
11651228
// The package name that we found in node_modules could be different from the package
11661229
// name in the package.json content via url/filepath dependency specifiers. We need to
11671230
// use the actual directory name, so don't look at `packageJsonContent.name` here.
1168-
const nodeModulesDirectoryName = packageRootPath.substring(parts.topLevelPackageNameIndex + 1);
1169-
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
1231+
const nodeModulesDirectoryName = packageRootPath.substring(parts!.topLevelPackageNameIndex! + 1);
1232+
const packageName = getPackageNameFromTypesPackageName(pnpPackageName ? pnpPackageName : nodeModulesDirectoryName);
11701233
const conditions = getConditions(options, importMode);
11711234
const fromExports = packageJsonContent?.exports
11721235
? tryGetModuleNameFromExports(options, host, path, packageRootPath, packageName, packageJsonContent.exports, conditions)
@@ -1229,7 +1292,7 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
12291292
}
12301293
else {
12311294
// No package.json exists; an index.js will still resolve as the package name
1232-
const fileName = getCanonicalFileName(moduleFileToTry.substring(parts.packageRootIndex + 1));
1295+
const fileName = getCanonicalFileName(moduleFileToTry.substring(parts!.packageRootIndex + 1));
12331296
if (fileName === "index.d.ts" || fileName === "index.js" || fileName === "index.ts" || fileName === "index.tsx") {
12341297
return { moduleFileToTry, packageRootPath };
12351298
}

‎src/compiler/pnp.ts

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
getDirectoryPath,
3+
resolvePath,
4+
} from "./path.js";
5+
import { getPnpApi } from "./pnpapi.js";
6+
7+
export function getPnpTypeRoots(currentDirectory: string) {
8+
const pnpApi = getPnpApi(currentDirectory);
9+
if (!pnpApi) {
10+
return [];
11+
}
12+
13+
// Some TS consumers pass relative paths that aren't normalized
14+
currentDirectory = resolvePath(currentDirectory);
15+
16+
const currentPackage = pnpApi.findPackageLocator(`${currentDirectory}/`);
17+
if (!currentPackage) {
18+
return [];
19+
}
20+
21+
const { packageDependencies } = pnpApi.getPackageInformation(currentPackage);
22+
23+
const typeRoots: string[] = [];
24+
for (const [name, referencish] of Array.from<any>(packageDependencies.entries())) {
25+
// eslint-disable-next-line no-restricted-syntax
26+
if (name.startsWith(`@types/`) && referencish !== null) {
27+
const dependencyLocator = pnpApi.getLocator(name, referencish);
28+
const { packageLocation } = pnpApi.getPackageInformation(dependencyLocator);
29+
30+
typeRoots.push(getDirectoryPath(packageLocation));
31+
}
32+
}
33+
34+
return typeRoots;
35+
}
36+
37+
export function isImportablePathPnp(fromPath: string, toPath: string): boolean {
38+
const pnpApi = getPnpApi(fromPath);
39+
40+
const fromLocator = pnpApi.findPackageLocator(fromPath);
41+
const toLocator = pnpApi.findPackageLocator(toPath);
42+
43+
// eslint-disable-next-line no-restricted-syntax
44+
if (toLocator === null) {
45+
return false;
46+
}
47+
48+
const fromInfo = pnpApi.getPackageInformation(fromLocator);
49+
const toReference = fromInfo.packageDependencies.get(toLocator.name);
50+
51+
if (toReference) {
52+
return toReference === toLocator.reference;
53+
}
54+
55+
// Aliased dependencies
56+
for (const reference of fromInfo.packageDependencies.values()) {
57+
if (Array.isArray(reference)) {
58+
if (reference[0] === toLocator.name && reference[1] === toLocator.reference) {
59+
return true;
60+
}
61+
}
62+
}
63+
64+
return false;
65+
}

‎src/compiler/pnpapi.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// To preserve the effects of https://github.com/microsoft/TypeScript/pull/55326
2+
// this file needs to avoid importing large graphs.
3+
4+
export function getPnpApi(path: string) {
5+
if (typeof process.versions.pnp === "undefined") {
6+
return;
7+
}
8+
9+
const { findPnpApi } = require("module");
10+
if (findPnpApi) {
11+
return findPnpApi(`${path}/`);
12+
}
13+
}
14+
15+
export function getPnpApiPath(path: string): string | undefined {
16+
// eslint-disable-next-line no-restricted-syntax
17+
return getPnpApi(path)?.resolveRequest("pnpapi", /*issuer*/ null);
18+
}

‎src/compiler/sys.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1723,6 +1723,10 @@ export let sys: System = (() => {
17231723
}
17241724

17251725
function isFileSystemCaseSensitive(): boolean {
1726+
// The PnP runtime is always case-sensitive
1727+
if (typeof process.versions.pnp !== `undefined`) {
1728+
return true;
1729+
}
17261730
// win32\win64 are case insensitive platforms
17271731
if (platform === "win32" || platform === "win64") {
17281732
return false;

‎src/compiler/utilities.ts

+9
Original file line numberDiff line numberDiff line change
@@ -10659,6 +10659,15 @@ export interface NodeModulePathParts {
1065910659
readonly packageRootIndex: number;
1066010660
readonly fileNameIndex: number;
1066110661
}
10662+
10663+
/** @internal */
10664+
export interface PackagePathParts {
10665+
readonly topLevelNodeModulesIndex: undefined;
10666+
readonly topLevelPackageNameIndex: undefined;
10667+
readonly packageRootIndex: number;
10668+
readonly fileNameIndex: number;
10669+
}
10670+
1066210671
/** @internal */
1066310672
export function getNodeModulePathParts(fullPath: string): NodeModulePathParts | undefined {
1066410673
// If fullPath can't be valid module file within node_modules, returns undefined.

0 commit comments

Comments
 (0)
Please sign in to comment.